--- /dev/null
+/attestation/c/.libs
+/attestation/c/autom4te.cache
+/attestation/c/build
+/attestation/c/include
+/attestation/c/modules
+/attestation/c/config.h
+/attestation/c/config.h.in
+/attestation/c/config.log
+/attestation/c/config.nice
+/attestation/c/config.status
+/attestation/c/configure
+/attestation/c/configure.ac
+/attestation/c/libtool
+/attestation/c/main.dep
+/attestation/c/main.lo
+/attestation/c/Makefile
+/attestation/c/Makefile.fragments
+/attestation/c/Makefile.objects
+/attestation/c/tpm_attestation.la
+/attestation/c/run-tests.php
+/attestation/cgo/libtpmattestation.h
+/attestation/cgo/libtpmattestation.so
+/attestation/php/vendor/
+/attestation/php/.phpunit.result.cache
+/attestation/php.ini
+
+attestation/client/client
+machines/event_monitor/event_monitor
+sase/ws_proxy_client/ws_proxy_client
+sase/ws_tcp_proxy/ws_tcp_proxy
+sd/health_exporter/health_exporter
+sd/sd_register/sd_register
+sd/sd_publish/sd_publish
+sd/sd_watcher/sd_watcher
+echo/server/server
+echo/client/client
+attestation/rpc_server/rpc_server
+attestation/rpc_client/rpc_client
+thirdparty/registry/registry
+metrics/metricbus/mbclient/example/example
+metrics/metricbus/mbserver/mbserver
+mtls/verify_tool/main
+metrics/apcups_exporter/apcups_exporter
+ansible/client/client
+ansible/admin_tool/admin_tool
+ansible/server/server
+mtls/verify_tool/verify_tool
+ldap/health_exporter/health_exporter
+envoy/xds/envoy_xds/envoy_xds
--- /dev/null
+GOMAINSRCS = $(shell find . -type f -name main.go | cut -c 3- | paste -s -d ' ' -)
+GOMAINDIRS = $(GOMAINSRCS:/main.go=)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+GOMAINS =
+
+ROOT_DOMAIN := fuhry.dev
+DEFAULT_REGION := hq
+DEFAULT_HOST_DOMAIN := $(DEFAULT_REGION).$(ROOT_DOMAIN)
+SD_DOMAIN := v.$(ROOT_DOMAIN)
+WEB_SERVICES_DOMAIN := $(ROOT_DOMAIN)
+MACHINES_HOST := machines.$(WEB_SERVICES_DOMAIN)
+MACHINES_MQTT_TOPIC := machines/events
+ROOT_CA_NAME := FooCorp Root
+INT_CA_NAME := FooCorp Intermediate mTLS
+DEVICE_TRUST_TOKEN_NAME := FooCorp Device Trust
+
+LDFLAGS :=
+
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.RootDomain=$(ROOT_DOMAIN)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.DefaultRegion=$(DEFAULT_REGION)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.DefaultHostDomain=$(DEFAULT_HOST_DOMAIN)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.SDDomain=$(SD_DOMAIN)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.WebServicesDomain=$(WEB_SERVICES_DOMAIN)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.MachinesHost=$(MACHINES_HOST)
+LDFLAGS += -X=go.fuhry.dev/runtime/constants.MachinesMqttTopic=$(MACHINES_MQTT_TOPIC)
+#LDFLAGS += -X="go.fuhry.dev/runtime/constants.RootCAName=$(ROOT_CA_NAME)"
+#LDFLAGS += -X="go.fuhry.dev/runtime/constants.IntCAName=$(INT_CA_NAME)"
+#LDFLAGS += -X="go.fuhry.dev/runtime/constants.DeviceTrustTokenName=$(DEVICE_TRUST_TOKEN_NAME)"
+
+define GOPROG_template =
+GOMAINS += $(1)/$(2)
+all: $(1)/$(2)
+$(1)/$(2): $(1)/main.go
+ go build -ldflags '$$(LDFLAGS)' $$(GOBUILDFLAGS) -o $$@ $$<
+
+endef
+
+$(foreach maindir,$(GOMAINDIRS),$(eval $(call GOPROG_template,$(maindir),$(shell basename $(maindir)))))
+
+.PHONY: $(GOMAINDIRS) clean all
+
+clean:
+ if test -z "$(GOMAINS)"; then \
+ echo "ERROR: Failed to discover main.go sources in tree." >&2; \
+ echo "Debug info:" >&2; \
+ echo " GOMAINSRCS = $(GOMAINSRCS)" >&2; \
+ echo " GOMAINDIRS = $(GOMAINDIRS)" >&2; \
+ exit 1; \
+ fi
+ rm -fv $(GOMAINS)
--- /dev/null
+CONFIGURE_ARGS :=
+
+all: cgo c
+
+.PHONY: clean
+clean:
+ /bin/rm -rfv \
+ c/.libs \
+ c/autom4te.cache \
+ c/build \
+ c/include \
+ c/modules
+
+ /bin/rm -fv \
+ c/config.h \
+ c/config.h.in \
+ c/config.log \
+ c/config.nice \
+ c/config.status \
+ c/configure \
+ c/configure.ac \
+ c/libtool \
+ c/main.dep \
+ c/main.lo \
+ c/Makefile \
+ c/Makefile.fragments \
+ c/Makefile.objects \
+ c/tpm_attestation.la \
+ c/run-tests.php \
+ cgo/libtpmattestation.h \
+ cgo/libtpmattestation.so \
+
+cgo/libtpmattestation.so:
+ cd ./cgo && go build -o libtpmattestation.so -buildmode=c-shared ./extension_api.go
+
+.PHONY: cgo
+cgo: cgo/libtpmattestation.so
+
+c/configure:
+ cd ./c && phpize
+
+c/Makefile: c/configure
+ cd ./c && ./configure $(CONFIGURE_ARGS)
+
+.PHONY: c
+c: c/configure c/Makefile cgo
+ cd ./c && EXTRA_CFLAGS="-I`pwd`/../cgo -L`pwd`/../cgo -ltpmattestation" make
+
+install: cgo c
+ cd ./c && make install
+
+php.ini: php.ini.in
+ sed -re 's;@PWD@;$(PWD);g' $< > $@
+
+.PHONY: test
+test: php.ini c
+ cd $(PWD)/php && \
+ env LD_LIBRARY_PATH=$(PWD)/cgo php -c $(PWD)/php.ini ./vendor/bin/phpunit
--- /dev/null
+PHP_ARG_ENABLE(tpm_attestation, Whether to enable the TPM Attestation extension, [ --enable-tpm-attestation Enable TPM Attestation module])
+
+if test "$TPM_ATTESTATION" != "no"; then
+ PHP_NEW_EXTENSION(tpm_attestation, main.c, $ext_shared)
+fi
--- /dev/null
+// include the PHP API itself
+#include <php.h>
+#include <zend_exceptions.h>
+
+#include "libtpmattestation.h"
+
+#define PHP_TPM_ATTESTATION_EXTNAME "tpm-attestation"
+#define PHP_TPM_ATTESTATION_VERSION "0.0.1"
+
+static zend_class_entry *attestation_parameters_class_entry = NULL;
+static zend_class_entry *activation_challenge_class_entry = NULL;
+static zend_class_entry *platform_parameters_class_entry = NULL;
+static zend_class_entry *quote_class_entry = NULL;
+static zend_class_entry *pcr_class_entry = NULL;
+
+#define COND_FREE_BYTES(x) { if (x.Data != NULL) free(x.Data); }
+#define ZVAL_STRING_DECLARE(var, value) \
+ zval var; \
+ ZVAL_STRING(&var, value);
+
+///////////////////////////////////////////////////////////////////////////////
+// C STRUCT INITIALIZATION/FREEING
+///////////////////////////////////////////////////////////////////////////////
+
+PlatformParameters* xa_platform_parameters_new(uint32_t quotes_count, uint32_t pcrs_count) {
+ PlatformParameters* pp = (PlatformParameters *)malloc(sizeof(PlatformParameters));
+ if (pp == NULL) {
+ return NULL;
+ }
+ memset(pp, 0, sizeof(PlatformParameters));
+
+ pp->Quotes = malloc(sizeof(Quote*) * (quotes_count + 1));
+ if (pp->Quotes == NULL) {
+ goto err0;
+ }
+ pp->PCRs = malloc(sizeof(PCR*) * (pcrs_count + 1));
+ if (pp->PCRs == NULL) {
+ goto err1;
+ }
+
+ for (int i = 0; i < quotes_count; i++) {
+ pp->Quotes[i] = malloc(sizeof(Quote));
+ if (pp->Quotes[i] == NULL) {
+ goto err2;
+ }
+ }
+ pp->Quotes[quotes_count] = NULL;
+
+ for (int i = 0; i < pcrs_count; i++) {
+ pp->PCRs[i] = malloc(sizeof(PCR));
+ if (pp->PCRs[i] == NULL) {
+ goto err3;
+ }
+ }
+ pp->PCRs[pcrs_count] = NULL;
+
+ return pp;
+
+err3:
+ for (int i = 0; pp->PCRs[i] != NULL; i++) {
+ free(pp->PCRs[i]);
+ }
+err2:
+ for (int i = 0; pp->Quotes[i] != NULL; i++) {
+ free(pp->Quotes[i]);
+ }
+ free(pp->PCRs);
+err1:
+ free(pp->Quotes);
+err0:
+ free(pp);
+ return NULL;
+}
+
+void xa_platform_parameters_free(PlatformParameters* pp) {
+ for (int i = 0; pp->Quotes[i] != NULL; i++) {
+ php_printf("free pp->Quotes[%d]\n", i);
+ free(pp->Quotes[i]);
+ }
+ php_printf("free pp->Quotes\n");
+ free(pp->Quotes);
+
+ for (int i = 0; pp->PCRs[i] != NULL; i++) {
+ php_printf("free pp->PCRs[%d]\n", i);
+ free(pp->PCRs[i]);
+ }
+ php_printf("free pp->PCRs\n");
+ free(pp->PCRs);
+}
+
+void xa_attestation_parameters_free(AttestationParameters* ap) {
+ if (ap == NULL) return;
+
+ free(ap);
+}
+
+void xa_activation_challenge_response_free(ActivationChallengeResponse* r) {
+ if (r == NULL) return;
+
+ COND_FREE_BYTES(r->Error);
+
+ if (r->Response != NULL) {
+ COND_FREE_BYTES(r->Response->Secret);
+ COND_FREE_BYTES(r->Response->EncryptedSecret);
+ COND_FREE_BYTES(r->Response->Credential);
+
+ free(r->Response);
+ }
+
+ free(r);
+}
+
+void xa_attest_platform_response_free(AttestPlatformResponse* r) {
+ if (r == NULL) return;
+
+ COND_FREE_BYTES(r->Error);
+ COND_FREE_BYTES(r->Response);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// C STRUCT CONVERSION TO PHP
+///////////////////////////////////////////////////////////////////////////////
+
+void cActivationChallengeToPhp(ActivationChallenge* c, zval* php) {
+ zend_class_entry *ce = activation_challenge_class_entry;
+ object_init_ex(php, activation_challenge_class_entry);
+
+ zval zSecret, zEncryptedSecret, zCredential;
+ ZVAL_STRINGL(&zSecret, c->Secret.Data, c->Secret.Length);
+ ZVAL_STRINGL(&zEncryptedSecret, c->EncryptedSecret.Data, c->EncryptedSecret.Length);
+ ZVAL_STRINGL(&zCredential, c->Credential.Data, c->Credential.Length);
+
+ zend_update_property(activation_challenge_class_entry, Z_OBJ_P(php), "Secret", sizeof("Secret")-1, &zSecret);
+ zend_update_property(activation_challenge_class_entry, Z_OBJ_P(php), "EncryptedSecret", sizeof("EncryptedSecret")-1, &zEncryptedSecret);
+ zend_update_property(activation_challenge_class_entry, Z_OBJ_P(php), "Credential", sizeof("Credential")-1, &zCredential);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// PHP OBJECT CONVERSION TO C STRUCTS
+///////////////////////////////////////////////////////////////////////////////
+
+AttestationParameters* phpAttestationParametersToC(zend_object* php) {
+ zval temp_;
+ AttestationParameters* c;
+
+ c = (AttestationParameters*)malloc(sizeof(AttestationParameters));
+ if (c == NULL) {
+ return c;
+ }
+ memset(c, 0, sizeof(AttestationParameters));
+
+ zval *zPublic = zend_read_property(attestation_parameters_class_entry, php, "Public", sizeof("Public")-1, 1, &temp_);
+ c->Public.Data = Z_STRVAL_P(zPublic);
+ c->Public.Length = Z_STRLEN_P(zPublic);
+
+ zval *zUseTCSDActivationFormat = zend_read_property(attestation_parameters_class_entry, php, "UseTCSDActivationFormat", sizeof("UseTCSDActivationFormat")-1, 1, &temp_);
+ c->UseTCSDActivationFormat = Z_LVAL_P(zUseTCSDActivationFormat);
+
+ zval *zCreateData = zend_read_property(attestation_parameters_class_entry, php, "CreateData", sizeof("CreateData")-1, 1, &temp_);
+ c->CreateData.Data = Z_STRVAL(*zCreateData);
+ c->CreateData.Length = Z_STRLEN(*zCreateData);
+
+ zval *zCreateAttestation = zend_read_property(attestation_parameters_class_entry, php, "CreateAttestation", sizeof("CreateAttestation")-1, 1, &temp_);
+ c->CreateAttestation.Data = Z_STRVAL(*zCreateAttestation);
+ c->CreateAttestation.Length = Z_STRLEN(*zCreateAttestation);
+
+ zval *zCreateSignature = zend_read_property(attestation_parameters_class_entry, php, "CreateSignature", sizeof("CreateSignature")-1, 1, &temp_);
+ c->CreateSignature.Data = Z_STRVAL(*zCreateSignature);
+ c->CreateSignature.Length = Z_STRLEN(*zCreateSignature);
+
+ return c;
+}
+
+void phpQuoteToC(zend_object* php, Quote* c) {
+ zval temp_;
+
+ zval *zVersion = zend_read_property(quote_class_entry, php, "Version", sizeof("Version") - 1, 1, &temp_);
+ c->Version = Z_LVAL_P(zVersion);
+
+ zval *zQuote = zend_read_property(quote_class_entry, php, "Quote", sizeof("Quote") - 1, 1, &temp_);
+ c->Quote.Data = Z_STRVAL(*zQuote);
+ c->Quote.Length = Z_STRLEN(*zQuote);
+
+ zval *zSignature = zend_read_property(quote_class_entry, php, "Signature", sizeof("Signature") - 1, 1, &temp_);
+ c->Signature.Data = Z_STRVAL(*zSignature);
+ c->Signature.Length = Z_STRLEN(*zSignature);
+}
+
+void phpPcrToC(zend_object* php, PCR* c) {
+ zval temp_;
+
+ zval *zIndex = zend_read_property(pcr_class_entry, php, "Index", sizeof("Index") - 1, 1, &temp_);
+ c->Index = Z_LVAL_P(zIndex);
+
+ zval *zDigestAlg = zend_read_property(pcr_class_entry, php, "DigestAlg", sizeof("DigestAlg") - 1, 1, &temp_);
+ c->DigestAlg = Z_LVAL_P(zDigestAlg);
+
+ zval *zDigest = zend_read_property(pcr_class_entry, php, "Digest", sizeof("Digest") - 1, 1, &temp_);
+ c->Digest.Data = Z_STRVAL(*zDigest);
+ c->Digest.Length = Z_STRLEN(*zDigest);
+}
+
+PlatformParameters* phpPlatformParametersToC(zend_object* php) {
+ zval temp_;
+ PlatformParameters* c;
+
+ zval *zQuotes = zend_read_property(platform_parameters_class_entry, php, "Quotes", sizeof("Quotes") - 1, 0, &temp_);
+ zval *zPCRs = zend_read_property(platform_parameters_class_entry, php, "PCRs", sizeof("PCRs") - 1, 0, &temp_);
+
+ if (UNEXPECTED(Z_TYPE_P(zQuotes) != IS_ARRAY)) {
+ zend_throw_exception(NULL, "PlatformParameters: \"Quotes\" property is not an array", 0);
+ return NULL;
+ }
+ if (UNEXPECTED(Z_TYPE_P(zPCRs) != IS_ARRAY)) {
+ zend_throw_exception(NULL, "PlatformParameters: \"PCRs\" property is not an array", 0);
+ return NULL;
+ }
+ c = xa_platform_parameters_new(zend_array_count(Z_ARR_P(zQuotes)), zend_array_count(Z_ARR_P(zPCRs)));
+ if (UNEXPECTED(c == NULL)) {
+ zend_throw_exception(NULL, "PlatformParameters: internal error: memory allocation failed for C struct", 0);
+ return NULL;
+ }
+
+ zval *zTPMVersion = zend_read_property(platform_parameters_class_entry, php, "TPMVersion", sizeof("TPMVersion") - 1, 0, &temp_);
+ c->TPMVersion = Z_LVAL_P(zTPMVersion);
+
+ zval *zPublic = zend_read_property(platform_parameters_class_entry, php, "Public", sizeof("Public") - 1, 0, &temp_);
+ c->Public.Data = Z_STRVAL(*zPublic);
+ c->Public.Length = Z_STRLEN(*zPublic);
+
+ zval *zEventLog = zend_read_property(platform_parameters_class_entry, php, "EventLog", sizeof("EventLog") - 1, 0, &temp_);
+ c->EventLog.Data = Z_STRVAL(*zEventLog);
+ c->EventLog.Length = Z_STRLEN(*zEventLog);
+
+ zend_ulong i;
+ void *_key;
+ zval *val;
+
+ ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zQuotes), i, _key, val) {
+ phpQuoteToC(Z_OBJ_P(val), c->Quotes[i]);
+ } ZEND_HASH_FOREACH_END();
+
+ ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zPCRs), i, _key, val) {
+ phpPcrToC(Z_OBJ_P(val), c->PCRs[i]);
+ } ZEND_HASH_FOREACH_END();
+
+ return c;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// AttestationParameters class
+///////////////////////////////////////////////////////////////////////////////
+
+// AttestationParameters constructor
+PHP_METHOD(AttestationParameters, __construct)
+{
+ zend_string* Public;
+ zend_bool UseTCSDActivationFormat;
+ zend_string* CreateData;
+ zend_string* CreateAttestation;
+ zend_string* CreateSignature;
+
+ ZEND_PARSE_PARAMETERS_START(5, 5)
+ Z_PARAM_STR(Public)
+ Z_PARAM_BOOL(UseTCSDActivationFormat)
+ Z_PARAM_STR(CreateData)
+ Z_PARAM_STR(CreateAttestation)
+ Z_PARAM_STR(CreateSignature)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_update_property_str(attestation_parameters_class_entry, Z_OBJ_P(ZEND_THIS), "Public", sizeof("Public")-1, Public);
+ zend_update_property_bool(attestation_parameters_class_entry, Z_OBJ_P(ZEND_THIS), "UseTCSDActivationFormat", sizeof("UseTCSDActivationFormat")-1, UseTCSDActivationFormat);
+ zend_update_property_str(attestation_parameters_class_entry, Z_OBJ_P(ZEND_THIS), "CreateData", sizeof("CreateData")-1, CreateData);
+ zend_update_property_str(attestation_parameters_class_entry, Z_OBJ_P(ZEND_THIS), "CreateAttestation", sizeof("CreateAttestation")-1, CreateAttestation);
+ zend_update_property_str(attestation_parameters_class_entry, Z_OBJ_P(ZEND_THIS), "CreateSignature", sizeof("CreateSignature")-1, CreateSignature);
+}
+ZEND_BEGIN_ARG_INFO(arginfo_attestation_parameters_construct, 4)
+ ZEND_ARG_INFO(0, Public)
+ ZEND_ARG_INFO(0, UseTCSDActivationFormat)
+ ZEND_ARG_INFO(0, CreateData)
+ ZEND_ARG_INFO(0, CreateAttestation)
+ ZEND_ARG_INFO(0, CreateSignature)
+ZEND_END_ARG_INFO()
+
+// AttestationParameters fromJson alternate constructor
+PHP_METHOD(AttestationParameters, fromJson)
+{
+ zval* inp = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY(inp)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zval *zPublic, *zUseTCSDActivationFormat, *zCreateData, *zCreateSignature, *zCreateAttestation;
+ ZVAL_STRING_DECLARE(iPublic, "Public");
+ ZVAL_STRING_DECLARE(iUseTCSDActivationFormat, "UseTCSDActivationFormat");
+ ZVAL_STRING_DECLARE(iCreateData, "CreateData");
+ ZVAL_STRING_DECLARE(iCreateAttestation, "CreateAttestation");
+ ZVAL_STRING_DECLARE(iCreateSignature, "CreateSignature");
+
+ zPublic = zend_hash_find(Z_ARR_P(inp), Z_STR(iPublic));
+ zUseTCSDActivationFormat = zend_hash_find(Z_ARR_P(inp), Z_STR(iUseTCSDActivationFormat));
+ zCreateData = zend_hash_find(Z_ARR_P(inp), Z_STR(iCreateData));
+ zCreateAttestation = zend_hash_find(Z_ARR_P(inp), Z_STR(iCreateAttestation));
+ zCreateSignature = zend_hash_find(Z_ARR_P(inp), Z_STR(iCreateSignature));
+
+ if (zPublic == NULL || Z_TYPE_P(zPublic) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"Public\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zUseTCSDActivationFormat == NULL || !(Z_TYPE_P(zUseTCSDActivationFormat) == IS_TRUE || Z_TYPE_P(zUseTCSDActivationFormat) == IS_FALSE)) {
+ zend_throw_exception(NULL, "Key \"UseTCSDActivationFormat\" must be present and must be a boolean.", 0);
+ return;
+ }
+ if (zCreateData == NULL || Z_TYPE_P(zCreateData) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"CreateData\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zCreateAttestation == NULL || Z_TYPE_P(zCreateAttestation) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"CreateAttestation\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zCreateSignature == NULL || Z_TYPE_P(zCreateSignature) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"CreateSignature\" must be present and must be a string.", 0);
+ return;
+ }
+
+ object_init_ex(return_value, attestation_parameters_class_entry);
+
+ zend_update_property(attestation_parameters_class_entry, Z_OBJ_P(return_value), "Public", sizeof("Public")-1, zPublic);
+ zend_update_property(attestation_parameters_class_entry, Z_OBJ_P(return_value), "UseTCSDActivationFormat", sizeof("UseTCSDActivationFormat")-1, zUseTCSDActivationFormat);
+ zend_update_property(attestation_parameters_class_entry, Z_OBJ_P(return_value), "CreateData", sizeof("CreateData")-1, zCreateData);
+ zend_update_property(attestation_parameters_class_entry, Z_OBJ_P(return_value), "CreateAttestation", sizeof("CreateAttestation")-1, zCreateAttestation);
+ zend_update_property(attestation_parameters_class_entry, Z_OBJ_P(return_value), "CreateSignature", sizeof("CreateSignature")-1, zCreateSignature);
+}
+ZEND_BEGIN_ARG_INFO(arginfo_attestation_parameters_fromjson, 0)
+ ZEND_ARG_INFO(0, json)
+ZEND_END_ARG_INFO()
+
+// GetActivationChallenge method
+PHP_METHOD(AttestationParameters, GetActivationChallenge)
+{
+ zend_long tpm_version;
+ zend_string* endorsement_key = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_LONG(tpm_version)
+ Z_PARAM_STR(endorsement_key)
+ ZEND_PARSE_PARAMETERS_END();
+
+ GoUint8 gTPMVersion = tpm_version;
+ Bytes gEndorsementKey;
+ gEndorsementKey.Data = ZSTR_VAL(endorsement_key);
+ gEndorsementKey.Length = ZSTR_LEN(endorsement_key);
+
+ AttestationParameters* ap = phpAttestationParametersToC(Z_OBJ_P(ZEND_THIS));
+
+ ActivationChallengeResponse* response = GetActivationChallenge(
+ gTPMVersion,
+ ap,
+ gEndorsementKey);
+
+ xa_attestation_parameters_free(ap);
+
+ if (response == NULL) {
+ zend_throw_exception(NULL, "Internal error: GetActivationChallenge() returned NULL", 0);
+ return;
+ }
+ if (response->Error.Data != NULL) {
+ xa_activation_challenge_response_free(response);
+ zend_throw_exception(NULL, response->Error.Data, 0);
+ return;
+ }
+
+ cActivationChallengeToPhp(response->Response, return_value);
+ xa_activation_challenge_response_free(response);
+}
+ZEND_BEGIN_ARG_INFO(arginfo_attestation_parameters_getactivationchallenge, 1)
+ ZEND_ARG_INFO(0, TPMVersion)
+ ZEND_ARG_INFO(0, EndorsementKey)
+ZEND_END_ARG_INFO()
+
+///////////////////////////////////////////////////////////////////////////////
+// ActivationChallenge class
+///////////////////////////////////////////////////////////////////////////////
+
+// getSecret returns the Secret. This should not be shared with the client.
+// It should be cached on the server and associated with the identity of the
+// client.
+PHP_METHOD(ActivationChallenge, getSecret)
+{
+ zval temp_;
+
+ zval *secret = zend_read_property(activation_challenge_class_entry, Z_OBJ_P(ZEND_THIS), "Secret", sizeof("Secret")-1, 1, &temp_);
+
+ if (secret == NULL) {
+ zend_throw_exception(NULL, "Internal error: Failed to read Secret property", 0);
+ return;
+ }
+
+ ZVAL_STR_COPY(return_value, Z_STR_P(secret));
+}
+ZEND_BEGIN_ARG_INFO(arginfo_activation_challenge_get_secret, 1)
+ZEND_END_ARG_INFO()
+
+// toJson returns an array containing the ActivationChallenge properties that
+// should be sent back to the client (EncryptedSecret and Credential).
+PHP_METHOD(ActivationChallenge, toJson)
+{
+ zval temp_;
+
+ zval *encrypted_secret = zend_read_property(activation_challenge_class_entry, Z_OBJ_P(ZEND_THIS), "EncryptedSecret", sizeof("EncryptedSecret")-1, 1, &temp_);
+ if (encrypted_secret == NULL) {
+ zend_throw_exception(NULL, "Internal error: Failed to read EncryptedSecret property", 0);
+ return;
+ }
+
+ zval *credential = zend_read_property(activation_challenge_class_entry, Z_OBJ_P(ZEND_THIS), "Credential", sizeof("Credential")-1, 1, &temp_);
+ if (credential == NULL) {
+ zend_throw_exception(NULL, "Internal error: Failed to read Credential property", 0);
+ return;
+ }
+
+ array_init_size(return_value, 2);
+ add_assoc_zval(return_value, "EncryptedSecret", encrypted_secret);
+ add_assoc_zval(return_value, "Credential", credential);
+
+}
+ZEND_BEGIN_ARG_INFO(arginfo_activation_challenge_to_json, 1)
+ZEND_END_ARG_INFO()
+
+///////////////////////////////////////////////////////////////////////////////
+// PlatformParameters class
+///////////////////////////////////////////////////////////////////////////////
+
+// Alternate fromJson constructor
+PHP_METHOD(PlatformParameters, fromJson)
+{
+ zval* inp = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY(inp)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zval *zTPMVersion,
+ *zPublic,
+ *zQuotesJ,
+ *zPCRsJ,
+ *zEventLog;
+ ZVAL_STRING_DECLARE(iTPMVersion, "TPMVersion");
+ ZVAL_STRING_DECLARE(iPublic, "Public");
+ ZVAL_STRING_DECLARE(iQuotes, "Quotes");
+ ZVAL_STRING_DECLARE(iPCRs, "PCRs");
+ ZVAL_STRING_DECLARE(iEventLog, "EventLog");
+
+ ZVAL_STRING_DECLARE(iVersion, "Version");
+ ZVAL_STRING_DECLARE(iQuote, "Quote");
+ ZVAL_STRING_DECLARE(iSignature, "Signature");
+
+ ZVAL_STRING_DECLARE(iIndex, "Index");
+ ZVAL_STRING_DECLARE(iDigest, "Digest");
+ ZVAL_STRING_DECLARE(iDigestAlg, "DigestAlg");
+
+ zTPMVersion = zend_hash_find(Z_ARR_P(inp), Z_STR(iTPMVersion));
+ zPublic = zend_hash_find(Z_ARR_P(inp), Z_STR(iPublic));
+ zQuotesJ = zend_hash_find(Z_ARR_P(inp), Z_STR(iQuotes));
+ zPCRsJ = zend_hash_find(Z_ARR_P(inp), Z_STR(iPCRs));
+ zEventLog = zend_hash_find(Z_ARR_P(inp), Z_STR(iEventLog));
+
+ if (zTPMVersion == NULL || Z_TYPE_P(zTPMVersion) != IS_LONG) {
+ zend_throw_exception(NULL, "Key \"TPMVersion\" must be present and must be an integer.", 0);
+ return;
+ }
+ if (zPublic == NULL || Z_TYPE_P(zPublic) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"Public\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zQuotesJ == NULL || Z_TYPE_P(zQuotesJ) != IS_ARRAY) {
+ zend_throw_exception(NULL, "Key \"Quotes\" must be present and must be an array.", 0);
+ return;
+ }
+ if (zPCRsJ == NULL || Z_TYPE_P(zPCRsJ) != IS_ARRAY) {
+ zend_throw_exception(NULL, "Key \"PCRs\" must be present and must be an array.", 0);
+ return;
+ }
+ if (zEventLog == NULL || Z_TYPE_P(zEventLog) != IS_STRING) {
+ zend_throw_exception(NULL, "Key \"EventLog\" must be present and must be a string.", 0);
+ return;
+ }
+
+ zval zQuotes, zPCRs;
+
+ array_init_size(&zQuotes, zend_array_count(Z_ARR_P(zQuotesJ)));
+ array_init_size(&zPCRs, zend_array_count(Z_ARR_P(zPCRsJ)));
+
+ {
+ zend_ulong i_;
+ void *k_;
+ zval *val;
+ ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zQuotesJ), i_, k_, val) {
+ if (Z_TYPE_P(val) != IS_ARRAY) {
+ zend_throw_exception(NULL, "All entries in the \"Quotes\" property must be arrays.", 0);
+ return;
+ }
+ zval *zVersion = zend_hash_find(Z_ARR_P(val), Z_STR(iVersion));
+ zval *zQuote = zend_hash_find(Z_ARR_P(val), Z_STR(iQuote));
+ zval *zSignature = zend_hash_find(Z_ARR_P(val), Z_STR(iSignature));
+
+ if (zVersion == NULL || Z_TYPE_P(zVersion) != IS_LONG) {
+ zend_throw_exception(NULL, "While parsing Quotes: Key \"Version\" must be present and must be an integer.", 0);
+ return;
+ }
+ if (zQuote == NULL || Z_TYPE_P(zQuote) != IS_STRING) {
+ zend_throw_exception(NULL, "While parsing Quotes: Key \"Quote\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zSignature == NULL || Z_TYPE_P(zSignature) != IS_STRING) {
+ zend_throw_exception(NULL, "While parsing Quotes: Key \"Signature\" must be present and must be a string.", 0);
+ return;
+ }
+
+ zval zVersionC, zQuoteC, zSignatureC;
+ ZVAL_LONG(&zVersionC, Z_LVAL_P(zVersion));
+ ZVAL_STR(&zQuoteC, Z_STR_P(zQuote));
+ ZVAL_STR(&zSignatureC, Z_STR_P(zSignature));
+
+ zval zQuoteObj;
+ object_init_ex(&zQuoteObj, quote_class_entry);
+ zend_update_property(quote_class_entry, Z_OBJ(zQuoteObj), "Version", sizeof("Version")-1, &zVersionC);
+ zend_update_property(quote_class_entry, Z_OBJ(zQuoteObj), "Quote", sizeof("Quote")-1, &zQuoteC);
+ zend_update_property(quote_class_entry, Z_OBJ(zQuoteObj), "Signature", sizeof("Signature")-1, &zSignatureC);
+
+
+ add_next_index_zval(&zQuotes, &zQuoteObj);
+
+ } ZEND_HASH_FOREACH_END();
+ }
+
+ {
+ zend_ulong i_;
+ void *k_;
+ zval *val;
+ ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(zPCRsJ), i_, k_, val) {
+ if (Z_TYPE_P(val) != IS_ARRAY) {
+ zend_throw_exception(NULL, "All entries in the \"PCRs\" property must be arrays.", 0);
+ return;
+ }
+ zval *zIndex = zend_hash_find(Z_ARR_P(val), Z_STR(iIndex));
+ zval *zDigest = zend_hash_find(Z_ARR_P(val), Z_STR(iDigest));
+ zval *zDigestAlg = zend_hash_find(Z_ARR_P(val), Z_STR(iDigestAlg));
+
+ if (zIndex == NULL || Z_TYPE_P(zIndex) != IS_LONG) {
+ zend_throw_exception(NULL, "While parsing PCRs: Key \"Index\" must be present and must be an integer.", 0);
+ return;
+ }
+ if (zDigest == NULL || Z_TYPE_P(zDigest) != IS_STRING) {
+ zend_throw_exception(NULL, "While parsing PCRs: Key \"Digest\" must be present and must be a string.", 0);
+ return;
+ }
+ if (zDigestAlg == NULL || Z_TYPE_P(zDigestAlg) != IS_LONG) {
+ zend_throw_exception(NULL, "While parsing PCRs: Key \"DigestAlg\" must be present and must be an integer.", 0);
+ return;
+ }
+
+ zval zIndexC, zDigestC, zDigestAlgC;
+ ZVAL_LONG(&zIndexC, Z_LVAL_P(zIndex));
+ ZVAL_STR(&zDigestC, Z_STR_P(zDigest));
+ ZVAL_LONG(&zDigestAlgC, Z_LVAL_P(zDigestAlg));
+
+ zval zPCRObj;
+ object_init_ex(&zPCRObj, pcr_class_entry);
+ zend_update_property(quote_class_entry, Z_OBJ(zPCRObj), "Index", sizeof("Index")-1, &zIndexC);
+ zend_update_property(quote_class_entry, Z_OBJ(zPCRObj), "Digest", sizeof("Digest")-1, &zDigestC);
+ zend_update_property(quote_class_entry, Z_OBJ(zPCRObj), "DigestAlg", sizeof("DigestAlg")-1, &zDigestAlgC);
+
+ add_next_index_zval(&zPCRs, &zPCRObj);
+
+ } ZEND_HASH_FOREACH_END();
+ }
+
+ object_init_ex(return_value, platform_parameters_class_entry);
+
+ zend_update_property(platform_parameters_class_entry, Z_OBJ_P(return_value), "TPMVersion", sizeof("TPMVersion")-1, zTPMVersion);
+ zend_update_property(platform_parameters_class_entry, Z_OBJ_P(return_value), "Public", sizeof("Public")-1, zPublic);
+ zend_update_property(platform_parameters_class_entry, Z_OBJ_P(return_value), "Quotes", sizeof("Quotes")-1, &zQuotes);
+ zend_update_property(platform_parameters_class_entry, Z_OBJ_P(return_value), "PCRs", sizeof("PCRs")-1, &zPCRs);
+ zend_update_property(platform_parameters_class_entry, Z_OBJ_P(return_value), "EventLog", sizeof("EventLog")-1, zEventLog);
+}
+ZEND_BEGIN_ARG_INFO(arginfo_platform_parameters_fromjson, 0)
+ ZEND_ARG_INFO(0, json)
+ZEND_END_ARG_INFO()
+
+// AttestPlatform
+PHP_METHOD(PlatformParameters, AttestPlatform)
+{
+ zend_string* nonce = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(nonce)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (nonce == NULL) {
+ zend_throw_exception(NULL, "The argument \"nonce\" to AttestPlatform must be a base64-encoded or binary string.", 0);
+ return;
+ }
+
+ Bytes gNonce;
+ gNonce.Data = ZSTR_VAL(nonce);
+ gNonce.Length = ZSTR_LEN(nonce);
+
+ PlatformParameters* pp = phpPlatformParametersToC(Z_OBJ_P(ZEND_THIS));
+
+ AttestPlatformResponse* response = AttestPlatform(gNonce, pp);
+
+ // xa_platform_parameters_free(pp);
+
+ if (response == NULL) {
+ zend_throw_exception(NULL, "Internal error: AttestPlatform() returned NULL", 0);
+ return;
+ }
+ if (response->Error.Data != NULL) {
+ zend_throw_exception(NULL, response->Error.Data, 0);
+ xa_attest_platform_response_free(response);
+ return;
+ }
+
+ ZVAL_STRINGL(return_value, response->Response.Data, response->Response.Length);
+ xa_attest_platform_response_free(response);
+}
+ZEND_BEGIN_ARG_INFO(arginfo_platform_parameters_attest_platform, 0)
+ ZEND_ARG_INFO(0, nonce)
+ZEND_END_ARG_INFO()
+
+///////////////////////////////////////////////////////////////////////////////
+// FUNCTION TABLES
+///////////////////////////////////////////////////////////////////////////////
+
+const zend_function_entry attestation_parameters_functions[] = {
+ // public methods
+ PHP_ME(AttestationParameters, __construct, arginfo_attestation_parameters_construct, ZEND_ACC_PUBLIC)
+ PHP_ME(AttestationParameters, fromJson, arginfo_attestation_parameters_fromjson, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)
+ PHP_ME(AttestationParameters, GetActivationChallenge, arginfo_attestation_parameters_getactivationchallenge, ZEND_ACC_PUBLIC)
+ PHP_FE_END
+};
+
+const zend_function_entry activation_challenge_functions[] = {
+ // public methods
+ // PHP_ME(ActivationChallenge, __construct, arginfo_activation_challenge_construct, ZEND_ACC_PUBLIC)
+ PHP_ME(ActivationChallenge, getSecret, arginfo_activation_challenge_get_secret, ZEND_ACC_PUBLIC)
+ PHP_ME(ActivationChallenge, toJson, arginfo_activation_challenge_to_json, ZEND_ACC_PUBLIC)
+ PHP_FE_END
+};
+
+const zend_function_entry platform_parameters_functions[] = {
+ // public methods
+ // PHP_ME(PlatformParameters, __construct, arginfo_platform_parameters_construct, ZEND_ACC_PUBLIC)
+ PHP_ME(PlatformParameters, fromJson, arginfo_platform_parameters_fromjson, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)
+ PHP_ME(PlatformParameters, AttestPlatform, arginfo_platform_parameters_attest_platform, ZEND_ACC_PUBLIC)
+ PHP_FE_END
+};
+
+const zend_function_entry quote_functions[] = {
+ // public methods
+ // PHP_ME(PlatformParameters, __construct, arginfo_platform_parameters_construct, ZEND_ACC_PUBLIC)
+ PHP_FE_END
+};
+
+const zend_function_entry pcr_functions[] = {
+ // public methods
+ // PHP_ME(PlatformParameters, __construct, arginfo_platform_parameters_construct, ZEND_ACC_PUBLIC)
+ PHP_FE_END
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// MODULE INITIALIZATION
+///////////////////////////////////////////////////////////////////////////////
+
+PHP_MINIT_FUNCTION(TPMAttestation)
+{
+ /* AttestationParameters */
+ zend_class_entry attestationParametersEntry;
+ INIT_NS_CLASS_ENTRY(attestationParametersEntry, "TPMAttestation", "AttestationParameters", attestation_parameters_functions);
+
+ attestation_parameters_class_entry = zend_register_internal_class(&attestationParametersEntry);
+ zend_declare_property_string(attestation_parameters_class_entry, "Public", strlen("Public"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_bool(attestation_parameters_class_entry, "UseTCSDActivationFormat", strlen("UseTCSDActivationFormat"), 0, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(attestation_parameters_class_entry, "CreateData", strlen("CreateData"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(attestation_parameters_class_entry, "CreateAttestation", strlen("CreateAttestation"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(attestation_parameters_class_entry, "CreateSignature", strlen("CreateSignature"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+
+ /* ActivationChallenge */
+ zend_class_entry activationChallengeEntry;
+ INIT_NS_CLASS_ENTRY(activationChallengeEntry, "TPMAttestation", "ActivationChallenge", activation_challenge_functions);
+
+ activation_challenge_class_entry = zend_register_internal_class(&activationChallengeEntry);
+ zend_declare_property_string(activation_challenge_class_entry, "Secret", strlen("Secret"), "", ZEND_ACC_PUBLIC);
+ zend_declare_property_string(activation_challenge_class_entry, "EncryptedSecret", strlen("EncryptedSecret"), "", ZEND_ACC_PUBLIC);
+ zend_declare_property_string(activation_challenge_class_entry, "Credential", strlen("Credential"), "", ZEND_ACC_PUBLIC);
+
+ /* PlatformParameters */
+ zend_class_entry platformParametersEntry;
+ INIT_NS_CLASS_ENTRY(platformParametersEntry, "TPMAttestation", "PlatformParameters", platform_parameters_functions);
+
+ platform_parameters_class_entry = zend_register_internal_class(&platformParametersEntry);
+ zend_declare_property_long(platform_parameters_class_entry, "TPMVersion", strlen("TPMVersion"), 0, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(platform_parameters_class_entry, "Public", strlen("Public"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(platform_parameters_class_entry, "EventLog", strlen("EventLog"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_null(platform_parameters_class_entry, "Quotes", strlen("Quotes"), ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_null(platform_parameters_class_entry, "PCRs", strlen("PCRs"), ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+
+ /* Quote */
+ zend_class_entry quoteEntry;
+ INIT_NS_CLASS_ENTRY(quoteEntry, "TPMAttestation", "Quote", quote_functions);
+
+ quote_class_entry = zend_register_internal_class("eEntry);
+ zend_declare_property_long(quote_class_entry, "Version", strlen("Version"), 0, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(quote_class_entry, "Quote", strlen("Quote"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(quote_class_entry, "Signature", strlen("Signature"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+
+ /* PCR */
+ zend_class_entry pcrEntry;
+ INIT_NS_CLASS_ENTRY(pcrEntry, "TPMAttestation", "PCR", pcr_functions);
+
+ pcr_class_entry = zend_register_internal_class(&pcrEntry);
+ zend_declare_property_long(pcr_class_entry, "Index", strlen("Index"), 0, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_long(pcr_class_entry, "DigestAlg", strlen("DigestAlg"), 0, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+ zend_declare_property_string(pcr_class_entry, "Digest", strlen("Digest"), "", ZEND_ACC_PUBLIC | ZEND_ACC_READONLY);
+
+ return SUCCESS;
+}
+
+// register our function to the PHP API
+// so that PHP knows, which functions are in this module
+zend_function_entry tpm_attestation_functions[] = {
+ // ZEND_NS_FE("TPMAttestation", receive_scalar, arginfo_receive_scalar)
+ PHP_FE_END // null termination
+};
+
+// some pieces of information about our module
+zend_module_entry tpm_attestation_module_entry = {
+ STANDARD_MODULE_HEADER,
+ PHP_TPM_ATTESTATION_EXTNAME,
+ tpm_attestation_functions,
+ PHP_MINIT(TPMAttestation),
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ PHP_TPM_ATTESTATION_VERSION,
+ STANDARD_MODULE_PROPERTIES
+};
+
+// argument must be prefix of *_module_entry
+ZEND_GET_MODULE(tpm_attestation);
--- /dev/null
+#ifndef TPM_ATTESTATION_EXTENSION_H
+#define TPM_ATTESTATION_EXTENSION_H
+
+#include <stdlib.h>
+#include <stdint.h>
+
+typedef struct Bytes {
+ size_t Length;
+ char* Data;
+} Bytes;
+
+typedef struct AttestationParameters {
+ Bytes Public;
+ int UseTCSDActivationFormat;
+ Bytes CreateData;
+ Bytes CreateAttestation;
+ Bytes CreateSignature;
+} AttestationParameters;
+
+typedef struct ActivationChallenge {
+ Bytes Secret;
+ Bytes EncryptedSecret;
+ Bytes Credential;
+} ActivationChallenge;
+
+typedef struct ActivationChallengeResponse {
+ ActivationChallenge* Response;
+ Bytes Error;
+} ActivationChallengeResponse;
+
+typedef struct Quote {
+ uint8_t Version;
+ Bytes Quote;
+ Bytes Signature;
+} Quote;
+
+typedef struct PCR {
+ int Index;
+ Bytes Digest;
+ uint32_t DigestAlg;
+} PCR;
+
+typedef struct PlatformParameters {
+ uint8_t TPMVersion;
+ Bytes Public;
+ Quote** Quotes;
+ PCR** PCRs;
+ Bytes EventLog;
+} PlatformParameters;
+
+typedef struct AttestPlatformResponse {
+ Bytes Response;
+ Bytes Error;
+} AttestPlatformResponse;
+
+#endif /* TPM_ATTESTATION_EXTENSION_H */
--- /dev/null
+package main
+
+// #include <stdlib.h>
+// #include <stdint.h>
+// #include <extension.h>
+import "C"
+
+import (
+ "crypto"
+ "encoding/base64"
+ "regexp"
+ "unsafe"
+
+ "github.com/google/go-attestation/attest"
+ "go.fuhry.dev/runtime/attestation/internal/attestation"
+)
+
+const (
+ RE_LOOKS_LIKE_BASE64 = "^[A-Za-z0-9/+]+={0,3}"
+)
+
+var regexpLooksLikeBase64 *regexp.Regexp
+
+//export GetActivationChallenge
+func GetActivationChallenge(tpmVersion uint8, attestationParameters *C.AttestationParameters, endorsementKey C.Bytes) *C.ActivationChallengeResponse {
+ req := &attestation.GetActivationChallenge_Request{
+ TPMVersion: tpmVersion,
+ EndorsementKey: C.GoStringN(endorsementKey.Data, C.int(endorsementKey.Length)),
+ AttestationParameters: cAttestationParamsToGo(attestationParameters),
+ }
+
+ cresp := newActivationChallengeResponse()
+ resp, err := attestation.GetActivationChallenge(req)
+ cresp.Response = nil
+ cresp.Error.Length = 0
+ cresp.Error.Data = nil
+
+ if err != nil {
+ cStrCpy(&cresp.Error, err.Error())
+ return cresp
+ }
+
+ cresp.Response = goActivationResponseToC(resp)
+ return cresp
+}
+
+//export AttestPlatform
+func AttestPlatform(decryptedSecret C.Bytes, platformParameters *C.PlatformParameters) *C.AttestPlatformResponse {
+ req := &attestation.AttestPlatform_Request{
+ DecryptedSecret: cBytesToGoBytes(decryptedSecret),
+ PlatformParameters: cPlatformParamsToGo(platformParameters),
+ }
+
+ cresp := newAttestPlatformResponse()
+ resp, err := attestation.AttestPlatform(req)
+
+ if err != nil {
+ cStrCpy(&cresp.Error, err.Error())
+ return cresp
+ }
+
+ cBytesCopy(&cresp.Response, resp.Nonce)
+ return cresp
+}
+
+func newActivationChallengeResponse() *C.ActivationChallengeResponse {
+ return (*C.ActivationChallengeResponse)(C.malloc(C.size_t(unsafe.Sizeof(C.ActivationChallengeResponse{}))))
+}
+
+func goActivationResponseToC(resp *attestation.GetActivationChallenge_Response) *C.ActivationChallenge {
+ cs := (*C.ActivationChallenge)(C.malloc(C.size_t(unsafe.Sizeof(C.ActivationChallenge{}))))
+
+ cBytesCopy(&cs.Secret, resp.Secret)
+ cBytesCopy(&cs.EncryptedSecret, resp.EncryptedSecret)
+ cBytesCopy(&cs.Credential, resp.Credential)
+
+ return cs
+}
+
+func newAttestPlatformResponse() *C.AttestPlatformResponse {
+ r := (*C.AttestPlatformResponse)(C.malloc(C.size_t(unsafe.Sizeof(C.AttestPlatformResponse{}))))
+
+ r.Response.Data = nil
+ r.Response.Length = 0
+
+ r.Error.Data = nil
+ r.Error.Length = 0
+
+ return r
+}
+
+func cAttestationParamsToGo(cAttParms *C.AttestationParameters) attest.AttestationParameters {
+ return attest.AttestationParameters{
+ Public: cBytesToGoBytes(cAttParms.Public),
+ UseTCSDActivationFormat: bool(cAttParms.UseTCSDActivationFormat != 0),
+ CreateData: cBytesToGoBytes(cAttParms.CreateData),
+ CreateAttestation: cBytesToGoBytes(cAttParms.CreateAttestation),
+ CreateSignature: cBytesToGoBytes(cAttParms.CreateSignature),
+ }
+}
+
+func cPlatformParamsToGo(cPlatParms *C.PlatformParameters) attest.PlatformParameters {
+ quotes := make([]attest.Quote, 0)
+ pcrs := make([]attest.PCR, 0)
+
+ cQuoteRefs := (*[1 << 32]*C.Quote)(unsafe.Pointer(cPlatParms.Quotes))
+ for i := 0; cQuoteRefs[i] != nil; i++ {
+ cQuote := cQuoteRefs[i]
+ quote := attest.Quote{
+ Version: attest.TPMVersion(cQuote.Version),
+ Quote: cBytesToGoBytes(cQuote.Quote),
+ Signature: cBytesToGoBytes(cQuote.Signature),
+ }
+ quotes = append(quotes, quote)
+ }
+
+ cPcrRefs := (*[1 << 32]*C.PCR)(unsafe.Pointer(cPlatParms.PCRs))
+ for i := 0; cPcrRefs[i] != nil; i++ {
+ cPCR := cPcrRefs[i]
+ pcr := attest.PCR{
+ Index: int(cPCR.Index),
+ DigestAlg: crypto.Hash(cPCR.DigestAlg),
+ Digest: cBytesToGoBytes(cPCR.Digest),
+ }
+ pcrs = append(pcrs, pcr)
+ }
+
+ return attest.PlatformParameters{
+ TPMVersion: attest.TPMVersion(cPlatParms.TPMVersion),
+ Public: cBytesToGoBytes(cPlatParms.Public),
+ Quotes: quotes,
+ PCRs: pcrs,
+ EventLog: cBytesToGoBytes(cPlatParms.EventLog),
+ }
+}
+
+func cStrCpy(cBytes *C.Bytes, src string) {
+ cBytes.Length = C.size_t(len(src))
+ cBytes.Data = C.CString(src)
+}
+
+func cBytesCopy(cBytes *C.Bytes, src []byte) {
+ srcB64 := base64.StdEncoding.EncodeToString(src)
+ cBytes.Data = C.CString(srcB64)
+ cBytes.Length = C.size_t(len(srcB64))
+}
+
+func cBytesToGoBytes(cb C.Bytes) []byte {
+ s := C.GoStringN(cb.Data, C.int(cb.Length))
+
+ if regexpLooksLikeBase64.MatchString(s) {
+ b, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return []byte{}
+ }
+ return b
+ }
+
+ return []byte(s)
+}
+
+func init() {
+ regexpLooksLikeBase64 = regexp.MustCompile(RE_LOOKS_LIKE_BASE64)
+}
+
+func main() {
+ panic("Don't call this directly")
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+getting a new access token, because: access token has expired
+fqWDVE7jfW3f7IqUsx9TgUC3lsAMFngn0JM1Em2SsSQ=
--- /dev/null
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "go.fuhry.dev/runtime/attestation/internal/attestation"
+)
+
+func main() {
+ flag.Parse()
+
+ nonce, err := attestation.AttestHost()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(nonce)
+}
--- /dev/null
+package attestation
+
+import (
+ "github.com/google/go-attestation/attest"
+)
+
+type GetActivationChallenge_Request struct {
+ TPMVersion uint8 `json:"tpm_version"`
+ AttestationParameters attest.AttestationParameters `json:"attestation_parameters"`
+ EndorsementKey string `json:"endorsement_key"`
+}
+
+type GetActivationChallenge_Response struct {
+ Secret []byte `json:"-"`
+ EncryptedSecret []byte `json:"EncryptedSecret"`
+ Credential []byte `json:"Credential"`
+}
+
+type AttestPlatform_Request struct {
+ DecryptedSecret []byte `json:"nonce"`
+ PlatformParameters attest.PlatformParameters `json:"platform_parameters"`
+}
+
+type AttestPlatform_Response struct {
+ Nonce []byte `json:"Nonce"`
+}
+
+func (r *GetActivationChallenge_Response) EC() attest.EncryptedCredential {
+ return attest.EncryptedCredential{
+ Credential: r.Credential,
+ Secret: r.EncryptedSecret,
+ }
+}
--- /dev/null
+package attestation
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "fmt"
+
+ "github.com/google/go-attestation/attest"
+ "go.fuhry.dev/runtime/machines"
+ attest_pb "go.fuhry.dev/runtime/proto/service/attest"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+func AttestHost() (string, error) {
+ machines, err := machines.NewDefaultMachinesClient("host.attestation.client")
+ if err != nil {
+ return "", err
+ }
+
+ // Open the TPM
+ tpm, err := attest.OpenTPM(nil)
+ if err != nil {
+ return "", err
+ }
+ defer tpm.Close()
+
+ // Create EK
+ eks, err := tpm.EKs()
+ if err != nil {
+ return "", err
+ }
+ ek := eks[0]
+
+ // Create AK
+ ak, err := tpm.NewAK(nil)
+ if err != nil {
+ return "", err
+ }
+
+ defer ak.Close(tpm)
+
+ // Marshal EK to bytes, assemble JSON request
+ ekBytes, err := marshalEK(ek)
+ if err != nil {
+ return "", err
+ }
+
+ activationParams := GetActivationChallenge_Request{
+ TPMVersion: uint8(tpm.Version()),
+ AttestationParameters: ak.AttestationParameters(),
+ EndorsementKey: string(ekBytes),
+ }
+
+ // Get activation parameters
+ activationChallenge := &GetActivationChallenge_Response{}
+ err = machines.APICall("host/attest/get_activation_challenge", activationParams, activationChallenge)
+ // activationChallenge, err := GetActivationChallenge(&activationParams)
+ if err != nil {
+ return "", fmt.Errorf("error calling rpc get_activation_challenge: %v", err)
+ }
+
+ decryptedSecret, err := ak.ActivateCredential(tpm, activationChallenge.EC())
+ if err != nil {
+ return "", fmt.Errorf("error decrypting the secret: %v", err)
+ }
+
+ attestation, err := tpm.AttestPlatform(ak, decryptedSecret, nil)
+ if err != nil {
+ return "", err
+ }
+
+ attestationParams := &AttestPlatform_Request{
+ DecryptedSecret: decryptedSecret,
+ PlatformParameters: *attestation,
+ }
+ attestationResponse := &AttestPlatform_Response{}
+ err = machines.APICall("host/attest/submit_quote", attestationParams, attestationResponse)
+ // attestationResponse, err := AttestPlatform(attestationParams)
+ if err != nil {
+ return "", err
+ }
+
+ return base64.StdEncoding.EncodeToString(attestationResponse.Nonce), nil
+}
+
+func AttestHostGrpc(ctx context.Context, attestCl attest_pb.AttestClient) (string, error) {
+ logger := log.WithPrefix("AttestHostGrpc")
+ logger.V(2).Infof("Opening TPM")
+ // Open the TPM
+ tpm, err := attest.OpenTPM(nil)
+ if err != nil {
+ return "", err
+ }
+ defer tpm.Close()
+
+ logger.V(2).Infof("Collecting endorsement key")
+ // Create EK
+ eks, err := tpm.EKs()
+ if err != nil {
+ return "", err
+ }
+ ek := eks[0]
+
+ logger.V(2).Infof("Creating attestation key")
+ // Create AK
+ ak, err := tpm.NewAK(nil)
+ if err != nil {
+ return "", err
+ }
+
+ defer ak.Close(tpm)
+
+ // Marshal EK to bytes, assemble JSON request
+ ekBytes, err := marshalEK(ek)
+ if err != nil {
+ return "", err
+ }
+
+ logger.V(2).Infof("Marshaled EK: %s", base64.StdEncoding.EncodeToString(ekBytes))
+ activationParams := &attest_pb.GetActivationChallengeRequest{
+ TpmVersion: uint32(tpm.Version()),
+ AttestationParameters: attest_pb.AttestationParametersToProto(ak.AttestationParameters()),
+ EndorsementKey: ekBytes,
+ }
+
+ // Get activation parameters
+ activationChallenge, err := attestCl.GetActivationChallenge(ctx, activationParams)
+ if err != nil {
+ return "", fmt.Errorf("error calling rpc get_activation_challenge: %v", err)
+ }
+ logger.V(2).Infof("Received encrypted activation challenge from server: %s", base64.StdEncoding.EncodeToString(activationChallenge.EncryptedSecret))
+
+ decryptedSecret, err := ak.ActivateCredential(tpm, activationChallenge.EC())
+ if err != nil {
+ return "", fmt.Errorf("error decrypting the secret: %v", err)
+ }
+ logger.V(2).Infof("Decrypted activation challenge to: %s", base64.StdEncoding.EncodeToString(decryptedSecret))
+
+ attestation, err := tpm.AttestPlatform(ak, decryptedSecret, nil)
+ if err != nil {
+ return "", err
+ }
+
+ logger.V(2).Infof("Quote: %+v", attestation.Quotes)
+ logger.V(2).Infof("PCRs: %+v", attestation.PCRs)
+ pp, err := attest_pb.PlatformParametersToProto(attestation)
+ if err != nil {
+ return "", err
+ }
+
+ attestationParams := &attest_pb.AttestPlatformRequest{
+ Nonce: decryptedSecret,
+ PlatformParameters: pp,
+ }
+ attestationResponse, err := attestCl.AttestPlatform(ctx, attestationParams)
+ if err != nil {
+ return "", err
+ }
+ logger.V(2).Noticef("Attestation successful! Nonce: %s", base64.RawStdEncoding.EncodeToString(attestationResponse.Nonce))
+
+ return base64.StdEncoding.EncodeToString(attestationResponse.Nonce), nil
+}
+
+func marshalEK(ek attest.EK) ([]byte, error) {
+ if ek.Certificate != nil {
+ return pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: ek.Certificate.Raw,
+ }), nil
+ }
+
+ switch pub := ek.Public.(type) {
+ case *ecdsa.PublicKey:
+ data, err := x509.MarshalPKIXPublicKey(pub)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling ec public key: %v", err)
+ }
+ return pem.EncodeToMemory(&pem.Block{
+ Type: "EC PUBLIC KEY",
+ Bytes: data,
+ }), nil
+
+ case *rsa.PublicKey:
+ return pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PUBLIC KEY",
+ Bytes: x509.MarshalPKCS1PublicKey(pub),
+ }), nil
+ default:
+ return nil, fmt.Errorf("unsupported public key type %T", pub)
+ }
+}
--- /dev/null
+package attestation
+
+import (
+ "context"
+ "crypto/subtle"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "time"
+
+ "google.golang.org/grpc/peer"
+
+ "go.fuhry.dev/runtime/grpc"
+ attest_pb "go.fuhry.dev/runtime/proto/service/attest"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+type sessionKey int
+
+const (
+ kExpectedSecret sessionKey = iota
+ kAttestTimestamp
+ kEndorsementKey
+ kAttestationKey
+ kHostGUID
+)
+
+type AttestServer struct {
+ attest_pb.UnimplementedAttestServer
+}
+
+type rpcClientInfo struct {
+ SPIFFEID *url.URL
+}
+
+type rpcStoreQuoteParams struct {
+ EK string `json:"endorsement_key"`
+ PP map[string]any `json:"platform_parameters"`
+}
+
+func NewAttestationServer() attest_pb.AttestServer {
+ return &AttestServer{}
+}
+
+func (s *AttestServer) GetActivationChallenge(ctx context.Context, req *attest_pb.GetActivationChallengeRequest) (*attest_pb.GetActivationChallengeResponse, error) {
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return nil, fmt.Errorf("provided context did not contain peer anything")
+ }
+
+ spiffe, err := grpc.PeerIdentity(peer)
+ if err != nil {
+ return nil, err
+ }
+
+ rci := &rpcClientInfo{
+ SPIFFEID: spiffe,
+ }
+ ek, err := unmarshalEK(string(req.EndorsementKey))
+ if err != nil {
+ return nil, err
+ }
+ hostGUID, err := verifyEK(ek, rci)
+ if err != nil {
+ return nil, fmt.Errorf("could not verify endorsement key with Machines API: %v", err)
+ }
+
+ reqNative := &GetActivationChallenge_Request{
+ TPMVersion: uint8(req.TpmVersion),
+ AttestationParameters: attest_pb.AttestationParametersFromProto(req.AttestationParameters),
+ EndorsementKey: string(req.EndorsementKey),
+ }
+
+ respNative, err := GetActivationChallenge(reqNative)
+ if err != nil {
+ return nil, err
+ }
+
+ session := grpc.SessionFromContext(ctx)
+ if session == nil {
+ return nil, fmt.Errorf("could not init session from connection context")
+ }
+ session.Set(kExpectedSecret, respNative.Secret)
+ session.Set(kAttestTimestamp, time.Now())
+ session.Set(kEndorsementKey, string(req.GetEndorsementKey()))
+ session.Set(kAttestationKey, req.AttestationParameters.Public)
+ session.Set(kHostGUID, hostGUID)
+
+ reply := &attest_pb.GetActivationChallengeResponse{
+ EncryptedSecret: respNative.EncryptedSecret,
+ Credential: respNative.Credential,
+ }
+
+ return reply, nil
+}
+
+func (s *AttestServer) AttestPlatform(ctx context.Context, req *attest_pb.AttestPlatformRequest) (*attest_pb.AttestPlatformResponse, error) {
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return nil, fmt.Errorf("provided context did not contain peer anything")
+ }
+
+ spiffe, err := grpc.PeerIdentity(peer)
+ if err != nil {
+ return nil, err
+ }
+
+ rci := &rpcClientInfo{
+ SPIFFEID: spiffe,
+ }
+
+ session := grpc.SessionFromContext(ctx)
+ if session == nil {
+ return nil, fmt.Errorf("could not init session from connection context")
+ }
+
+ secret, ok := session.Get(kExpectedSecret)
+ if !ok {
+ return nil, fmt.Errorf("activation challenge was not yet requested during this session")
+ }
+ if string(secret.([]uint8)) != string(req.Nonce) {
+ return nil, fmt.Errorf("activation challenge response does not match the expected value")
+ }
+ ak, ok := session.Get(kAttestationKey)
+ if !ok {
+ return nil, fmt.Errorf("could not get attestation key from session")
+ }
+ if subtle.ConstantTimeCompare(ak.([]byte), req.PlatformParameters.Public) == 0 {
+ return nil, fmt.Errorf("attestation key that signed quote is different from the attestation key on file for the session")
+ }
+ timestamp, ok := session.Get(kAttestTimestamp)
+ if !ok {
+ return nil, fmt.Errorf("could not get timestamp of activation challenge")
+ }
+ if timestamp.(time.Time).Add(5 * time.Second).Before(time.Now()) {
+ return nil, fmt.Errorf("client took too long to attest")
+ }
+ hostGUID, ok := session.Get(kHostGUID)
+ if !ok {
+ return nil, fmt.Errorf("could not get host guid")
+ }
+
+ pp, err := attest_pb.PlatformParametersFromProto(req.PlatformParameters)
+ if err != nil {
+ return nil, err
+ }
+
+ ek, ok := session.Get(kEndorsementKey)
+ if !ok {
+ return nil, fmt.Errorf("endorsement key not in session")
+ }
+
+ storeParams := &rpcStoreQuoteParams{
+ EK: ek.(string),
+ PP: mustProtoMarshal(req.PlatformParameters),
+ }
+
+ err = storeQuote(rci, hostGUID.(string), storeParams)
+ if err != nil {
+ return nil, err
+ }
+
+ reqNative := &AttestPlatform_Request{
+ DecryptedSecret: req.Nonce,
+ PlatformParameters: *pp,
+ }
+
+ respNative, err := AttestPlatform(reqNative)
+ if err != nil {
+ return nil, err
+ }
+
+ reply := &attest_pb.AttestPlatformResponse{
+ Nonce: respNative.Nonce,
+ }
+
+ return reply, nil
+}
+
+func mustProtoMarshalSlice[T protoreflect.ProtoMessage](messages []T) []map[string]any {
+ results := make([]map[string]any, 0)
+ for _, msg := range messages {
+ results = append(results, mustProtoMarshal(msg))
+ }
+
+ return results
+}
+
+func mustProtoMarshal(msg protoreflect.ProtoMessage) map[string]any {
+ marshaler := &protojson.MarshalOptions{
+ EmitUnpopulated: true,
+ UseEnumNumbers: true,
+ }
+
+ marshaled, err := marshaler.Marshal(msg)
+ if err != nil {
+ panic(err)
+ }
+ unmarshaled := make(map[string]any, 0)
+ err = json.Unmarshal(marshaled, &unmarshaled)
+ if err != nil {
+ panic(err)
+ }
+ return unmarshaled
+}
--- /dev/null
+package attestation
+
+import (
+ "crypto/dsa"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/hex"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/google/certificate-transparency-go/x509"
+ "github.com/google/go-attestation/attest"
+ "go.fuhry.dev/runtime/machines"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+var logger *log.Logger
+
+func init() {
+ logger = log.WithPrefix("AttestationServer")
+}
+
+func GetActivationChallenge(req *GetActivationChallenge_Request) (*GetActivationChallenge_Response, error) {
+ ek, err := unmarshalEK(req.EndorsementKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal endorsement key: %v", err)
+ }
+
+ ap := attest.ActivationParameters{
+ TPMVersion: attest.TPMVersion(req.TPMVersion),
+ EK: ek.Public,
+ AK: req.AttestationParameters,
+ }
+
+ secret, encryptedChallenge, err := ap.Generate()
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate activation challenge: %v", err)
+ }
+
+ return &GetActivationChallenge_Response{
+ Secret: secret,
+ EncryptedSecret: encryptedChallenge.Secret,
+ Credential: encryptedChallenge.Credential,
+ }, nil
+}
+
+func AttestPlatform(req *AttestPlatform_Request) (*AttestPlatform_Response, error) {
+ errs := make([]error, 0)
+ errStr := ""
+
+ pub, err := attest.ParseAKPublic(req.PlatformParameters.TPMVersion, req.PlatformParameters.Public)
+ if err != nil {
+ return nil, err
+ }
+
+ for i, quote := range req.PlatformParameters.Quotes {
+ if err = pub.Verify(quote, req.PlatformParameters.PCRs, req.DecryptedSecret); err != nil {
+ errStr += fmt.Sprintf("\n %v", err)
+ errs = append(errs, fmt.Errorf("failed to verify quote %d: %v", i, err))
+ }
+ }
+ if len(errs) > 0 {
+ return nil, fmt.Errorf("failed to verify signature on %d of %d quotes: %s",
+ len(errs),
+ len(req.PlatformParameters.Quotes),
+ errStr)
+ }
+
+ nonce := make([]byte, 32)
+ _, err = rand.Reader.Read(nonce)
+ if err != nil {
+ return nil, err
+ }
+ return &AttestPlatform_Response{
+ Nonce: nonce,
+ }, nil
+}
+
+func unmarshalEK(ekStr string) (*attest.EK, error) {
+ pemBlock, _ := pem.Decode([]byte(ekStr))
+
+ if pemBlock == nil {
+ return nil, errors.New("failed to decode PEM block containing endorsement key")
+ }
+
+ switch pemBlock.Type {
+ case "CERTIFICATE":
+ cert, err := x509.ParseCertificate(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ return &attest.EK{
+ Public: cert.PublicKey,
+ Certificate: cert,
+ CertificateURL: "",
+ }, nil
+ case "RSA PUBLIC KEY":
+ publicKey, err := x509.ParsePKCS1PublicKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return &attest.EK{
+ Public: publicKey,
+ Certificate: nil,
+ CertificateURL: "",
+ }, nil
+ case "EC PUBLIC KEY", "PUBLIC KEY":
+ publicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return &attest.EK{
+ Public: publicKey,
+ Certificate: nil,
+ CertificateURL: "",
+ }, nil
+ }
+
+ return nil, fmt.Errorf("fnable to unmarshal EK with PEM block type: %s", pemBlock.Type)
+}
+
+func verifyEK(ek *attest.EK, clientInfo *rpcClientInfo) (string, error) {
+ client, err := machines.NewDefaultMachinesClient("read", "host.read.notowned", "host.attestation.server")
+ if err != nil {
+ logger.Error("failed to get machines client:", err)
+ return "", fmt.Errorf("failed to get machines client: %v", err)
+ }
+ logger.V(1).Infof("looking up host %q in Machines API", clientInfo.SPIFFEID.Hostname())
+ hostname := strings.Split(clientInfo.SPIFFEID.Hostname(), ".")
+ if len(hostname) < 2 {
+ return "", fmt.Errorf("failed to split hostname in url")
+ }
+ udn := hostname[0]
+ machinesHost := &machines.Host{}
+
+ err = client.APICall("/host/"+udn, nil, machinesHost)
+ if err != nil {
+ logger.Errorf("failed to lookup host %q with machines API: %+v", udn, err)
+ return "", err
+ }
+ if machinesHost.ID == "" {
+ logger.Errorf("failed to lookup host %q with machines API: API call returned OK, but host ID is empty", udn, err)
+ return "", fmt.Errorf("cannot get host UUID from Machines API")
+ }
+ logger.V(1).Infof("host %q has machines UUID %s", udn, machinesHost.ID)
+
+ machinesEK := &machines.EndorsementKey{}
+ err = client.APICall("/host/"+machinesHost.ID+"/endorsement_key", nil, machinesEK)
+ if err != nil {
+ logger.Errorf("failed to retrieve endorsement key for host %q from machines API: %+v", udn, err)
+ return "", err
+ }
+
+ if !machinesEK.Found {
+ // logger.Errorf("host %q does not have an endorsement key on file with Machines", udn)
+ // return "", fmt.Errorf("host %q does not have an endorsement key on file with Machines", udn)
+ // if the host doesn't have an endorsement key on file, we TOFU.
+ return machinesHost.ID, nil
+ }
+
+ expectFingerprint := machinesEK.EndorsementKey.Fingerprint.SHA256.AsBytes()
+ hash := sha256.New()
+ if ek.Certificate != nil {
+ logger.V(1).Infof("expected fingerprint for host %q EK certificate: %s", udn, machinesEK.EndorsementKey.Fingerprint.SHA256)
+ hash.Write(ek.Certificate.Raw)
+ } else {
+ switch pubKey := ek.Public.(type) {
+ case *rsa.PublicKey:
+ logger.V(1).Infof("expected fingerprint for host %q EK RSA public key: %s", udn, machinesEK.EndorsementKey.Fingerprint.SHA256)
+ pubKeyDER := x509.MarshalPKCS1PublicKey(pubKey)
+ hash.Write(pubKeyDER)
+ case *dsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
+ logger.V(1).Infof("expected fingerprint for host %q EK PKIX (%T) public key: %s", udn, pubKey, machinesEK.EndorsementKey.Fingerprint.SHA256)
+ pubKeyDER, err := x509.MarshalPKIXPublicKey(pubKey)
+ if err != nil {
+ return "", err
+ }
+ hash.Write(pubKeyDER)
+ default:
+ return "", fmt.Errorf("unsupported type for public key: %T", pubKey)
+ }
+ }
+
+ actualFingerprint := hash.Sum(nil)
+ logger.V(1).Infof(" actual fingerprint: %s", hex.EncodeToString(actualFingerprint))
+ result := subtle.ConstantTimeCompare(actualFingerprint, expectFingerprint) == 1
+
+ if !result {
+ logger.Noticef("host %q: fingerprint mismatch, rejecting EK", udn)
+ return "", fmt.Errorf("endorsement key or certificate doesn't match the expected fingerprint")
+ }
+
+ logger.Noticef("host %q: fingerprint matched, proceeding with attestation", udn)
+ return machinesHost.ID, nil
+}
+
+func storeQuote(rci *rpcClientInfo, hostGUID string, quote *rpcStoreQuoteParams) error {
+ client, err := machines.NewDefaultMachinesClient("read", "host.read.notowned", "host.attestation.server")
+ if err != nil {
+ logger.Error("failed to get machines client:", err)
+ return fmt.Errorf("failed to get machines client: %v", err)
+ }
+
+ logger.V(1).Infof("posting attestation results to Machines API")
+ route := fmt.Sprintf("/host/%s/tpm_quote", hostGUID)
+ resp := make(map[string]any, 0)
+ err = client.APICall(route, quote, &resp)
+ if err != nil {
+ logger.Error("failed to POST %s: %v:", route, err)
+ return fmt.Errorf("failed to POST %s: %v", route, err)
+ }
+ logger.V(1).Debugf("response: %+v", resp)
+
+ return nil
+}
--- /dev/null
+extension=@PWD@/c/modules/tpm_attestation.so
--- /dev/null
+{
+ "name": "fuhry/attestation-tests",
+ "description": "Unit tests for tpm-attestation PHP extension",
+ "type": "project",
+ "require": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "TPMAttestation\\": "src/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Dan Fuhry",
+ "email": "dan@fuhry.com"
+ }
+ ]
+}
--- /dev/null
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "b2246d07a6a2beb6fe595cc1a1a498b1",
+ "packages": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
+ "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.22"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-03-03T08:28:38+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
+ "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-03-03T13:19:32+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.15.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
+ "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
+ },
+ "time": "2022-09-04T07:30:47+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+ },
+ "time": "2021-07-20T11:28:43+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.17",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8",
+ "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.14",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "*",
+ "ext-xdebug": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-08-30T12:24:04+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.5.25",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
+ "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.13",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.5",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^3.2",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-09-25T03:44:45+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:10:38+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
+ "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-04-03T09:37:03+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T06:03:37+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2",
+ "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-02-14T08:28:10+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-28T06:42:11+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:17:30+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
+ "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-12T14:47:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2021-07-28T10:34:58+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.2.0"
+}
--- /dev/null
+<?xml version="1.0" ?>
+<phpunit bootstrap="vendor/autoload.php" colors="true">
+ <testsuites>
+ <testsuite name="Base application tests">
+ <directory>./tests</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>
--- /dev/null
+<?php
+declare(strict_types=1);
+
+namespace TPMAttestation;
+
+/**
+ * Stub for the class provided by the extension.
+ */
+class ActivationChallenge
+{
+ private readonly string $Secret;
+ private readonly string $EncryptedSecret;
+ private readonly string $Credential;
+
+ public function getSecret(): string
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+
+ return $this->Secret;
+ }
+
+ public function toJson(): array
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+
+ return [
+ 'EncryptedSecret' => $this->EncryptedSecret,
+ 'Credential' => $this->Credential,
+ ];
+ }
+}
--- /dev/null
+<?php
+declare(strict_types=1);
+
+namespace TPMAttestation;
+
+/**
+ * Stub for the class provided by the extension.
+ */
+class AttestationParameters
+{
+ private readonly string $Public;
+ private readonly bool $UseTCSDActivationFormat;
+ private readonly string $CreateData;
+ private readonly string $CreateAttestation;
+ private readonly string $CreateSignature;
+
+ public function __construct(string $Public, bool $UseTCSDActivationFormat, string $CreateData, string $CreateAttestation, string $CreateSignature)
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+
+ $this->Public = $Public;
+ $this->UseTCSDActivationFormat = $UseTCSDActivationFormat;
+ $this->CreateData = $CreateData;
+ $this->CreateAttestation = $CreateAttestation;
+ $this->CreateSignature = $CreateSignature;
+ }
+
+ public static function fromJson(array $input): AttestationParameters
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+
+ assert(
+ 'array_key_exists("Public", $input) && is_string($input["Public"])',
+ new \InvalidArgumentException('Input argument "Public" must be a string.'),
+ );
+ assert(
+ 'array_key_exists("UseTCSDActivationFormat", $input) && is_bool($input["UseTCSDActivationFormat"])',
+ new \InvalidArgumentException('Input argument "UseTCSDActivationFormat" must be a bool.'),
+ );
+ assert(
+ 'array_key_exists("CreateData", $input) && is_string($input["CreateData"])',
+ new \InvalidArgumentException('Input argument "CreateData" must be a string.'),
+ );
+ assert(
+ 'array_key_exists("CreateAttestation", $input) && is_string($input["CreateAttestation"])',
+ new \InvalidArgumentException('Input argument "CreateAttestation" must be a string.'),
+ );
+ assert(
+ 'array_key_exists("CreateSignature", $input) && is_string($input["CreateSignature"])',
+ new \InvalidArgumentException('Input argument "CreateSignature" must be a string.'),
+ );
+
+ return new static(
+ $input['Public'],
+ $input['UseTCSDActivationFormat'],
+ $input['CreateData'],
+ $input['CreateAttestation'],
+ $input['CreateSignature'],
+ );
+ }
+
+ public function GetActivationChallenge(int $TPMVersion, string $EndorsementKey): ActivationChallenge
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+ }
+}
--- /dev/null
+<?php
+declare(strict_types=1);
+
+namespace TPMAttestation;
+
+/**
+ * Stub for the class provided by the extension.
+ */
+class PlatformParameters
+{
+ private readonly int $TPMVersion;
+ private readonly string $Public;
+ private readonly array $Quotes;
+ private readonly array $PCRs;
+ private readonly string $Event;
+
+ public static function fromJson(array $input): PlatformParameters
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+ }
+
+ public function AttestPlatform(string $nonce): string
+ {
+ throw new \RuntimeException("tpm_attestation PHP extension is not loaded");
+ }
+}
--- /dev/null
+<?php
+declare(strict_types=1);
+
+namespace Test\TPMAttestation;
+
+use PHPUnit\Framework\TestCase;
+use TPMAttestation;
+
+class ActivationChallengeTest extends TestCase
+{
+ private const TEST_ATTESTATION_PARAMETERS = [
+ "Public" => "AAEACwAFAHIAAAAQABQACwgAAAAAAAEAvF9ozm7YHprZ7Kn07RmkGY4j0y2Tp8DcFoRHy/hRYgn71J7ekpeNURzzRqOW8FC32kkI8YM8Spar6Kuoha/WIeV8WpsjQWakPWvGsgklQ6n+bLNw21Wtsr32FWDoeSrVtYqZdxhl4sjvhsNyCyZCx42+M50PSK7wacblmIWL77jWBxKXOH9wSXGJUhMdwcGaxR3ssSq9LoMTpB4xCkeGaCZlDFOp2EIwqYQ+8ynY6nib52zk6YsdQhYDEoKufzB1NHY9tSbs4BzLnPXCuOoUc1x/j59YoWp6P07qqjgtVGaBZeJgSwTZDsGVIbuF0GnmuMaqBzww8NLrkdrP+foJmw==",
+ "UseTCSDActivationFormat" => false,
+ "CreateData" => "AAAAAAAg47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUBAAsAIgALvz8Av3L699ylBtsyYu0leFCYir+27LSTjHaZy7tD0cMAIgALPl5I4xDjvW0rzuEHrMzXniTMm7xEyjcUVzdOni5TvqoAAA==",
+ "CreateAttestation" => "/1RDR4AaACIACzUBlsmEH4dLGdPbwG2gRgY+lTpBZFbUaB0T0vHJ1hB/AAAAAAACAH30yCC1fl866T6QAXVTLCCw87SUACIAC6z9ARzEd7TT+RyehMhlm9iCXPv5tydzKpm/Pj4beMbgACDvzHq6AIWsfFBAnCSOuez5miCsGSPKuD+nRU/L41sgBQ==",
+ "CreateSignature" => "ABQACwEAl2+9kjGvoyY9Ex0veWcaZnx5DyrYFenx1+4H1PRc38CD+mUy1WALdnsxaOa9Isua+8v9xmzGsA8TVUzPp//vmT9QIQyakzPDoEh/zWwgvG39OEWZI8PSy4Ury2m2X1f4G+hwJnjcKAkfN4/D8VtZvAqo5smZ2TB/AdbETbcnPCb3V+o74WUWkoNVawBffUHPnrEDTFQiDmm5ZYSHbFWn13tY/wLaraOMI9oQuyQNsnjzA4JuLlaofjF8yMBIka2p8Di6Kk3WqrhwXt9tr0Ud3LFm+xer66edJ5TrtH2Qqu1VL20galvTjIisIdPXnT6Y7tx7/Pn25gEXflPAFNa1Mg=="
+ ];
+
+ private const TEST_ENDORSEMENT_KEY = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAmXMGD+hiG/OP7D7+DxKRlN1Y2xtb2OZ+i0RrVio0pYfmGieCS0Fh\ngi8G4ZEiOZfqs7dZS4Xcr7Ab2AtEPM3GctyQU1z8g9wdmW9SENWKcfyoicuZD84x\nxCKwV/QWfBmTnNTCrByS3WZ26XRcnSJg81bLOLENKI+grpjP2fi9Eihpx9lcyloH\nwJR8Hf2/9q/NdgFta6s8L9v0B8zT/ucsrOrpCenRfJpR3fntq8j0d2K68wgkkX+I\ntDWwimce4spEzgf5OKBM29UpaLM+artKDLO8/mO8g9C7FlremUkjQgrW1VBL+AeL\n/rEEMOKTxTDQqTqFXVMZ0wdjodEIFSBnhQIDAQAB\n-----END RSA PUBLIC KEY-----\n";
+
+ public function testAttestationParameters_Construct()
+ {
+ $ap = new TPMAttestation\AttestationParameters(
+ base64_decode(self::TEST_ATTESTATION_PARAMETERS['Public']),
+ self::TEST_ATTESTATION_PARAMETERS['UseTCSDActivationFormat'],
+ base64_decode(self::TEST_ATTESTATION_PARAMETERS['CreateData']),
+ base64_decode(self::TEST_ATTESTATION_PARAMETERS['CreateAttestation']),
+ base64_decode(self::TEST_ATTESTATION_PARAMETERS['CreateSignature']),
+ );
+
+ $this->assertTrue(true, "Ensure no crashes/errors constructing AttestationParameters");
+ }
+
+ public function testAttestationParameters_FromJson()
+ {
+ $ap = TPMAttestation\AttestationParameters::fromJson(self::TEST_ATTESTATION_PARAMETERS);
+
+ $this->assertTrue(true, "Ensure no crashes/errors calling AttestationParameters::fromJson");
+ }
+
+ public function testAttestationParameters_GetActivationChallenge()
+ {
+ $ap = TPMAttestation\AttestationParameters::fromJson(self::TEST_ATTESTATION_PARAMETERS);
+
+ $challenge = $ap->GetActivationChallenge(
+ TPMVersion: 2,
+ EndorsementKey: self::TEST_ENDORSEMENT_KEY,
+ );
+
+ $this->assertInstanceOf(
+ 'TPMAttestation\\ActivationChallenge',
+ $challenge,
+ "Ensure GetActivationChallenge returns an ActivationChallenge",
+ );
+
+ $this->assertIsString(
+ $challenge->getSecret(),
+ "Ensure [ActivationChallenge]->getSecret() returns a string",
+ );
+
+ $this->assertEquals(
+ 32,
+ strlen(base64_decode($challenge->getSecret())),
+ "Ensure [ActivationChallenge]->getSecret() returns a base64 string that decodes to 32 bytes",
+ );
+
+ $challengeJson = $challenge->toJson();
+
+ $this->assertIsArray(
+ $challengeJson,
+ "Ensure [ActivationChallenge]->toJson() returns an array",
+ );
+
+ $this->assertArrayHasKey(
+ "EncryptedSecret",
+ $challengeJson,
+ "Ensure the array returned from [ActivationChallenge]->toJson() contains the key \"EncryptedSecret\"",
+ );
+ $this->assertArrayHasKey(
+ "Credential",
+ $challengeJson,
+ "Ensure the array returned from [ActivationChallenge]->toJson() contains the key \"Credential\"",
+ );
+
+ $this->assertNotEmpty(
+ $challengeJson['EncryptedSecret'],
+ "Ensure the \"EncryptedSecret\" key in the array returned from [ActivationChallenge]->toJson() is not an empty value",
+ );
+
+ $this->assertNotEmpty(
+ $challengeJson['Credential'],
+ "Ensure the \"Credential\" key in the array returned from [ActivationChallenge]->toJson() is not an empty value",
+ );
+ }
+}
--- /dev/null
+<?php
+declare(strict_types=1);
+
+namespace Test\TPMAttestation;
+
+use PHPUnit\Framework\TestCase;
+use TPMAttestation;
+
+class AttestPlatformTest extends TestCase
+{
+ public function testFromJson()
+ {
+ $pp = TPMAttestation\PlatformParameters::fromJson(self::TEST_PLATFORM_PARAMETERS);
+
+ $this->assertTrue(
+ true,
+ "Ensure PlatformParameters::fromJson() does not crash",
+ );
+ }
+
+ public function testAttestPlatform()
+ {
+ $pp = TPMAttestation\PlatformParameters::fromJson(self::TEST_PLATFORM_PARAMETERS);
+
+ $response = $pp->AttestPlatform(self::TEST_NONCE);
+
+ $this->assertTrue(
+ true,
+ "Ensure [PlatformParameters]->AttestPlatform(nonce) does not crash",
+ );
+
+ $this->assertIsString(
+ $response,
+ "Ensure [PlatformParameters]->AttestPlatform(nonce) returns a string",
+ );
+ }
+
+ private const TEST_NONCE = "nUBAaMJTv3qezSAwU0eaLz17+qbZHJQ7RpSj3BH+UuY=";
+
+ private const TEST_PLATFORM_PARAMETERS = [
+ "TPMVersion" => 2,
+ "Public" => "AAEACwAFAHIAAAAQABQACwgAAAAAAAEAvF9ozm7YHprZ7Kn07RmkGY4j0y2Tp8DcFoRHy/hRYgn71J7ekpeNURzzRqOW8FC32kkI8YM8Spar6Kuoha/WIeV8WpsjQWakPWvGsgklQ6n+bLNw21Wtsr32FWDoeSrVtYqZdxhl4sjvhsNyCyZCx42+M50PSK7wacblmIWL77jWBxKXOH9wSXGJUhMdwcGaxR3ssSq9LoMTpB4xCkeGaCZlDFOp2EIwqYQ+8ynY6nib52zk6YsdQhYDEoKufzB1NHY9tSbs4BzLnPXCuOoUc1x/j59YoWp6P07qqjgtVGaBZeJgSwTZDsGVIbuF0GnmuMaqBzww8NLrkdrP+foJmw==",
+ "Quotes" => [
+ [
+ "Version" => 2,
+ "Quote" => "/1RDR4AYACIACzUBlsmEH4dLGdPbwG2gRgY+lTpBZFbUaB0T0vHJ1hB/ACCdQEBowlO/ep7NIDBTR5ovPXv6ptkclDtGlKPcEf5S5gAAAAIAfgeDILV+XzrpPpABdVMsILDztJQAAAABAAsD////ACChyQuEISoiKHhZgA1qkbz10Pd4zWxtFpgW7cqlhAt5tA==",
+ "Signature" => "ABQACwEABv2XOyTPOT47AAgNnQf/c0MYmuKg8XUpPvu3bs/RkAJVIzVRJzTp7SZjY2MRgtzhDB4FFulkc14wxffBAAnpscRPST9lt08U+CuaOOJ2h+i8dBZopjGp47uO5xpIAjde8YDxwaM06vBeq7d2JbBmw7yDotpIOr9PwUJTNjFrcO+W2wRWRbDxhrNNIK2sarw150RlJaXqooO2wMaJ6RLEXtdextAw2sw3mE5wo3G/ZWF3Jsnxjmvc2UImyCamfygZq2FsvJe3NIf2LH7RNMdJ1DqS4IkcAQczhP6+uygxZbZXaB4qR1/UInN+Zz0iB0IcTBXf6JfF+yamGSlVacAJyQ=="
+ ]
+ ],
+ "PCRs" => [
+ [
+ "Index" => 0,
+ "Digest" => "C1UuJAxpdqe6QDRHdnOiqM6+UPtT6cdj1sz8Gxh72w8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 1,
+ "Digest" => "crvGVY+Kb4sSQspNeQSZFijDtURU66ohOn1JGqqWRVM=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 2,
+ "Digest" => "h53sTS6fjal84A/XYd+yKBS5Kb/Eac80l526s3HsQS4=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 3,
+ "Digest" => "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 4,
+ "Digest" => "Ugu0qGz8kaEGo4JzPuEeUi6oDvRURX3KzoiLsMF5900=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 5,
+ "Digest" => "dTpuqel+2R+t9ov7H8bozbDtbWhnT0n1D066RrTrMDk=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 6,
+ "Digest" => "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 7,
+ "Digest" => "Zcr43R4Op6Y0e2NdKzeck7mhNR7cKvw+zacA5TTrMGg=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 8,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 9,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 10,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 11,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 12,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 13,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 14,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 15,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 16,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 17,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 18,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 19,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 20,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 21,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 22,
+ "Digest" => "//////////////////////////////////////////8=",
+ "DigestAlg" => 5
+ ],
+ [
+ "Index" => 23,
+ "Digest" => "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "DigestAlg" => 5
+ ]
+ ],
+ "EventLog" => "AAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEAAABTcGVjIElEIEV2ZW50MDMAAAAAAAACaQIBAAAACwAgAAAAAAAACAAAAAEAAAALANaBg3s7j1/Whb+tBCynjmiE4Q4cjyctvM0v0QvnOKf6EAAAAAUrEKfH2WVBgUAq3elK9jwAAAAACgAAgAEAAAALAMIg63LJISnaQV3Iscvm3BD6CyXI2rbWSCxfjDQlM19kOgAAAClGdihGNjQ5RkMyRC1DMEU2LTQyNjItQUQ1MS0wQ0U2NEY3NjQyOUYpAADw4/8AAAAAABAAAAAAAAAAAAAACgAAgAEAAAALAHrZXcGI+0sKY50tovsY8/dgt9pn05fHyHIYE8CVAK5IGAAAAAdGVl9NQUlOAPCW/wAAAAAAAE0AAAAAAAcAAAABAACAAQAAAAsAEVqoJ9vM+0TSFq2ez9pWvepiC4YKlL7Vt6J7uhxNAtg1AAAAYd/ki8qT0hGqDQDgmAMrjAoAAAAAAAAAAQAAAAAAAABTAGUAYwB1AHIAZQBCAG8AbwB0AAAHAAAAAQAAgAEAAAALAN6nuAq1Oj2qok1cxGxk4fqf/QNzn5Cq29jAhnxKW0iQJAAAAGHf5IvKk9IRqg0A4JgDK4wCAAAAAAAAAAAAAAAAAAAAUABLAAcAAAABAACAAQAAAAsA5nDhIfzr1HO4vEG7gBMB/B2a+jOQTwb3FJt08SxHpo8mAAAAYd/ki8qT0hGqDQDgmAMrjAMAAAAAAAAAAAAAAAAAAABLAEUASwAHAAAAAQAAgAEAAAALALr4mjzKzlJ1DF8BKDUeBCKkFZehrf1QgiqjY7nRJOp8JAAAAMuyGdc6PZZFo7za0A5nZW8CAAAAAAAAAAAAAAAAAAAAZABiAAcAAAABAACAAQAAAAsAn3W2gjv/avECSk4gNnGc3VSNPLwr8d6OfvTQ7QH5S/kmAAAAy7IZ1zo9lkWjvNrQDmdlbwMAAAAAAAAAAAAAAAAAAABkAGIAeAACAAAABAAAgAEAAAALACAiLxQAjKtymiTP3UtYCGXMA4mG8SU44ZujvTllIRfQYAAAABiAJ7gAAAAAQN8DAAAAAAAAAAAAAAAAAEAAAAAAAAAAAgEMANBBAwoAAAAAAQEGAAIBAQEGAAAAAQEGAAACAQEGAAAABAgYAAAAAAA4JAQAAAAAAP+tBQAAAAAAf/8EAAIAAAAEAACAAQAAAAsAICIvFACMq3KaJM/dS1gIZcwDiYbxJTjhm6O9OWUhF9BgAAAAGMAfuAAAAABA3wMAAAAAAAAAAAAAAAAAQAAAAAAAAAACAQwA0EEDCgAAAAABAQYAAgEBAQYAAAABAQYAAAIBAQYAAQAECBgAAAAAADgkBAAAAAAA/60FAAAAAAB//wQAAgAAAAQAAIABAAAACwDROp0WRGEQzyLSkGXul1JhU3Bq6mgnsBQvDgQtcAst5lQAAAAY0Bm4AAAAADAIAgAAAAAAAAAAAAAAAAA0AAAAAAAAAAIBDADQQQMKAAAAAAEBBgABAwEBBgAAAAQIGAAAAAAAUO4AAAAAAAD//QEAAAAAAH//BAABAAAACwAAgAEAAAALAMWz18LdF038SIoFomhwnzcEEvwcIqXPI4PQq4R6ltLOLQAAAAxTbWJpb3NUYWJsZQABAAAAAAAAAEQV/fKUlyxKmS7lu88g45QAYMK9AAAAAAEAAAACAACAAQAAAAsAdXRZK/2SUxsZKg10+iMww421l6ZGC7S3EPxXJqPveA02AAAAYd/ki8qT0hGqDQDgmAMrjAkAAAAAAAAABAAAAAAAAABCAG8AbwB0AE8AcgBkAGUAcgANAAAAAQAAAAIAAIABAAAACwAdDBXeKzqKXtU6u+bDfL7gweCMCM1l7whmz26JvNTg9dgAAABh3+SLypPSEaoNAOCYAyuMCAAAAAAAAACoAAAAAAAAAEIAbwBvAHQAMAAwADAARAABAAAAdABXAGkAbgBkAG8AdwBzACAAQgBvAG8AdAAgAE0AYQBuAGEAZwBlAHIAAAAEASoAAQAAAAAIAAAAAAAAAKAAAAAAAABIg0lyTHYJTo6XtLqK+sX+AgIEBEYAXABFAEYASQBcAE0ASQBDAFIATwBTAE8ARgBUAFwAQgBPAE8AVABcAEIATwBPAFQATQBHAEYAVwAuAEUARgBJAAAAf/8EAAAAQk8BAAAAAgAAgAEAAAALAD1YICWU0ftRzMa4uS/NmYJ+f5YX0fUAj9dGfXVhi1tKXAEAAGHf5IvKk9IRqg0A4JgDK4wIAAAAAAAAACwBAAAAAAAAQgBvAG8AdAAwADAAMAAwAAAAAAB0AFcAaQBuAGQAbwB3AHMAIABCAG8AbwB0ACAATQBhAG4AYQBnAGUAcgAAAAQBKgACAAAAAKAPAAAAAAAAGAMAAAAAAD1ISwTCBEdJkirOelJg2NcCAgQERgBcAEUARgBJAFwATQBJAEMAUgBPAFMATwBGAFQAXABCAE8ATwBUAFwAQgBPAE8AVABNAEcARgBXAC4ARQBGAEkAAAB//wQAV0lORE9XUwABAAAAiAAAAHgAAABCAEMARABPAEIASgBFAEMAVAA9AHsAOQBkAGUAYQA4ADYAMgBjAC0ANQBjAGQAZAAtADQAZQA3ADAALQBhAGMAYwAxAC0AZgAzADIAYgAzADQANABkADQANwA5ADUAfQAAAGUAAQAAABAAAAAEAAAAf/8EAAQAAAAHAACAAQAAAAsAPWdytPhO1HWV1yosTF/9FfW7csdQf+JvKq7ixp1WM7ooAAAAQ2FsbGluZyBFRkkgQXBwbGljYXRpb24gZnJvbSBCb290IE9wdGlvbgAAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAEAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAIAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAMAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAQAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAUAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAYAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAcAAAAEAAAAAQAAAAsA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERkEAAAAAAAAAAUAAAAGAACAAQAAAAsAM2TfBzWh8Z22gN2Jm/i8RZwHjduKFvufTYcOCFrAlE/kAQAARUZJIFBBUlQAAAEAXAAAAGeu+S4AAAAAAQAAAAAAAACvEp47AAAAACIAAAAAAAAAjhKeOwAAAADhFsZynpQRSL0UcTu/+iHoAgAAAAAAAACAAAAAgAAAALdTBqADAAAAAAAAAChzKsEf+NIRuksAoMk+yTtIg0lyTHYJTo6XtLqK+sX+AAgAAAAAAAD/pwAAAAAAAAAAAAAAAAAAZQBmAGkAXwBzAHkAcwB0AGUAbQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArz3GD4OEckeOeT1p2Ed95Lt3+lC3L+VPkX3G/fcySuYAqAAAAAAAAP+nCAAAAAAAAAAAAAAAAABhAHIAYwBoAF8AYgBvAG8AdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB509bmB/XCRKI8I48qPfkowdSexdzQMUy0raq767VDXQCoCAAAAAAAjhKeOwAAAAAAAAAAAAAAAGEAcgBjAGgAXwBjAHIAeQBwAHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADAACAAQAAAAsA3PpbTjjnZJYSu82CxRaFUgmR1Bxw4puy5atpjNE4fmG8AAAAGED5tgAAAAAA4AEAAAAAAAAAAAAAAAAAnAAAAAAAAAACAQwA0EEDCgAAAAABAQYAAQEBAQYAAAADFxAAAQAAAAAlOFOBshmCBAEqAAEAAAAACAAAAAAAAACgAAAAAAAASINJckx2CU6Ol7S6ivrF/gICBARGAFwARQBGAEkAXABNAEkAQwBSAE8AUwBPAEYAVABcAEIATwBPAFQAXABCAE8ATwBUAE0ARwBGAFcALgBFAEYASQAAAH//BAAFAAAABwAAgAEAAAALANgEPWt7ha01jrO2rmqHOrfvI6JjUsXcT6pa7trPXrQbHQAAAEV4aXQgQm9vdCBTZXJ2aWNlcyBJbnZvY2F0aW9uBQAAAAcAAIABAAAACwC1T3VCy9hyqBqdneqDmyuNdHx+vV6mYVxA9C9EptvroCgAAABFeGl0IEJvb3QgU2VydmljZXMgUmV0dXJuZWQgd2l0aCBTdWNjZXNz"
+ ];
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os/signal"
+ "syscall"
+
+ "go.fuhry.dev/runtime/attestation/internal/attestation"
+ "go.fuhry.dev/runtime/grpc"
+ "go.fuhry.dev/runtime/mtls"
+ attest_pb "go.fuhry.dev/runtime/proto/service/attest"
+)
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ flag.Parse()
+
+ clientId := mtls.DefaultIdentity()
+ serverId := mtls.NewServiceIdentity("attest")
+ client, err := grpc.NewGrpcClient(ctx, serverId, clientId)
+ if err != nil {
+ panic(err)
+ }
+
+ conn, err := client.Conn()
+ if err != nil {
+ panic(err)
+ }
+
+ defer conn.Close()
+ attestCl := attest_pb.NewAttestClient(conn)
+
+ nonce, err := attestation.AttestHostGrpc(ctx, attestCl)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(nonce)
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+DEFAULT:
+ - service: attest
+ - user: '*'
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "syscall"
+
+ "go.fuhry.dev/runtime/attestation/internal/attestation"
+ "go.fuhry.dev/runtime/grpc"
+ "go.fuhry.dev/runtime/mtls"
+ attest_pb "go.fuhry.dev/runtime/proto/service/attest"
+
+ google_grpc "google.golang.org/grpc"
+)
+
+func main() {
+ var err error
+
+ flag.Parse()
+
+ serverIdentity := mtls.DefaultIdentity()
+ s, err := grpc.NewGrpcServer(serverIdentity)
+ if err != nil {
+ panic(err)
+ }
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ err = s.PublishAndServe(ctx, func(s *google_grpc.Server) {
+ attest_pb.RegisterAttestServer(s, attestation.NewAttestationServer())
+ })
+ if err != nil {
+ panic(err)
+ }
+ defer s.Stop()
+
+ <-ctx.Done()
+}
+
+func init() {
+ mtls.SetDefaultIdentity("attest")
+}
--- /dev/null
+package constants
+
+var (
+ RootDomain = "fuhry.dev"
+ DefaultRegion = "hq"
+ DefaultHostDomain = DefaultRegion + "." + RootDomain
+ SDDomain = "v." + RootDomain
+ WebServicesDomain = RootDomain
+ MachinesHost = "machines." + WebServicesDomain
+ MachinesMqttTopic = "machines/events"
+
+ RootCAName = "FooCorp Root"
+ IntCAName = "FooCorp Intermediate mTLS"
+ DeviceTrustTokenName = "FooCorp Device Trust"
+)
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "syscall"
+
+ "go.fuhry.dev/runtime/grpc"
+ "go.fuhry.dev/runtime/mtls"
+ echo_pb "go.fuhry.dev/runtime/proto/service/echo"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ flag.Parse()
+ logger := log.Default().WithPrefix("EchoClient")
+
+ clientId := mtls.DefaultIdentity()
+ serverId := mtls.NewServiceIdentity("echo")
+ client, err := grpc.NewGrpcClient(ctx, serverId, clientId)
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ conn, err := client.Conn()
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ defer conn.Close()
+ echoCl := echo_pb.NewEchoClient(conn)
+
+ req := &echo_pb.EchoRequest{Message: "hello world"}
+ logger.Noticef("client sending Echo: %+v", req)
+ result, err := echoCl.Echo(ctx, req)
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ logger.Noticef("server replied: %s", result.Message)
+
+ logger.Infof("sending a GreetRequest")
+ greetResult, err := echoCl.Greet(ctx, &echo_pb.GreetRequest{})
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ logger.Noticef("server replied: %s", greetResult.Message)
+}
--- /dev/null
+package echo
+
+import (
+ "context"
+ "fmt"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+ "google.golang.org/grpc/peer"
+
+ grpc_lib "go.fuhry.dev/runtime/grpc"
+ echo_pb "go.fuhry.dev/runtime/proto/service/echo"
+)
+
+type EchoServer struct {
+ echo_pb.UnimplementedEchoServer
+}
+
+func NewEchoServer() echo_pb.EchoServer {
+ return &EchoServer{}
+}
+
+func (s *EchoServer) Echo(ctx context.Context, req *echo_pb.EchoRequest) (*echo_pb.EchoReply, error) {
+ reply := &echo_pb.EchoReply{
+ Message: cases.Title(language.English).String(req.Message),
+ }
+
+ return reply, nil
+}
+
+func (s *EchoServer) Greet(ctx context.Context, req *echo_pb.GreetRequest) (*echo_pb.GreetReply, error) {
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return nil, fmt.Errorf("provided context did not contain peer anything")
+ }
+
+ spiffe, err := grpc_lib.PeerIdentity(peer)
+ if err != nil {
+ return nil, err
+ }
+
+ return &echo_pb.GreetReply{
+ Message: fmt.Sprintf("Hello, %s!", spiffe),
+ }, nil
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+DEFAULT:
+ - service: echo
+
+fuhry.runtime.service.echo.Echo/Echo:
+ - user: '*'
+ - service: echo
+
+fuhry.runtime.service.echo.Echo/Greet:
+ - service: '*'
+ - user: '*'
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "syscall"
+
+ "go.fuhry.dev/runtime/echo"
+ "go.fuhry.dev/runtime/grpc"
+ "go.fuhry.dev/runtime/mtls"
+ echo_pb "go.fuhry.dev/runtime/proto/service/echo"
+
+ google_grpc "google.golang.org/grpc"
+)
+
+func main() {
+ var err error
+
+ flag.Parse()
+
+ serverIdentity := mtls.DefaultIdentity()
+ s, err := grpc.NewGrpcServer(serverIdentity)
+ if err != nil {
+ panic(err)
+ }
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ err = s.PublishAndServe(ctx, func(s *google_grpc.Server) {
+ echo_pb.RegisterEchoServer(s, echo.NewEchoServer())
+ })
+ if err != nil {
+ panic(err)
+ }
+ defer s.Stop()
+
+ <-ctx.Done()
+}
+
+func init() {
+ mtls.SetDefaultIdentity("echo")
+}
--- /dev/null
+module go.fuhry.dev/runtime
+
+go 1.19
+
+require (
+ github.com/google/certificate-transparency-go v1.1.4
+ github.com/google/go-attestation v0.4.3
+ github.com/google/go-tpm v0.3.3 // indirect
+ github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad // indirect
+ golang.org/x/crypto v0.14.0
+ golang.org/x/sys v0.13.0 // indirect
+)
+
+require (
+ github.com/ThalesIgnite/crypto11 v1.2.5
+ github.com/distribution/distribution/v3 v3.0.0-20230621170613-87b280718d38
+ github.com/eclipse/paho.mqtt.golang v1.4.2
+ github.com/go-kit/log v0.2.1
+ github.com/godbus/dbus/v5 v5.1.0
+ github.com/gorilla/websocket v1.5.0
+ github.com/hashicorp/golang-lru v0.5.4
+ github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100
+ github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58
+ github.com/miekg/dns v1.1.50
+ github.com/prometheus/client_golang v1.14.0
+ github.com/prometheus/exporter-toolkit v0.8.1
+ github.com/quic-go/quic-go v0.39.0
+ github.com/sirupsen/logrus v1.9.0
+ github.com/urfave/cli/v2 v2.23.5
+ go.etcd.io/etcd/client/v3 v3.5.5
+ go.fuhry.dev/fsnotify v1.7.2
+ go.fuhry.dev/grpc-quic v0.1.2
+ golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
+ golang.org/x/sync v0.3.0
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
+ gopkg.in/yaml.v3 v3.0.1
+ howett.net/plist v1.0.0
+)
+
+require (
+ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect
+ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
+ github.com/docker/go-metrics v0.0.1 // indirect
+ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect
+ github.com/felixge/httpsnoop v1.0.1 // indirect
+ github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
+ github.com/go-logfmt/logfmt v0.5.1 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+ github.com/gomodule/redigo v1.8.2 // indirect
+ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+ github.com/gorilla/handlers v1.5.1 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
+ github.com/jpillora/backoff v1.0.0 // indirect
+ github.com/klauspost/compress v1.16.5 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+ github.com/miekg/pkcs11 v1.1.1 // indirect
+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
+ github.com/onsi/ginkgo/v2 v2.9.5 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.0.2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.37.0 // indirect
+ github.com/prometheus/procfs v0.8.0 // indirect
+ github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
+ github.com/rogpeppe/go-internal v1.9.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/stretchr/testify v1.8.4 // indirect
+ github.com/thales-e-security/pool v0.0.2 // indirect
+ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
+ go.uber.org/mock v0.3.0 // indirect
+ golang.org/x/mod v0.11.0 // indirect
+ golang.org/x/oauth2 v0.10.0 // indirect
+ golang.org/x/term v0.13.0 // indirect
+ golang.org/x/tools v0.10.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
+
+require (
+ github.com/coreos/go-semver v0.3.0 // indirect
+ github.com/coreos/go-systemd/v22 v22.4.0 // indirect
+ github.com/go-ldap/ldap/v3 v3.4.4
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/hashicorp/golang-lru/v2 v2.0.2
+ go.etcd.io/etcd/api/v3 v3.5.5 // indirect
+ go.etcd.io/etcd/client/pkg/v3 v3.5.5
+ go.uber.org/atomic v1.9.0
+ go.uber.org/multierr v1.8.0 // indirect
+ go.uber.org/zap v1.21.0 // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.org/x/text v0.13.0
+ google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
+ google.golang.org/grpc v1.58.3
+ google.golang.org/protobuf v1.31.0
+)
+
+replace github.com/keybase/go-keychain => github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d
--- /dev/null
+bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w=
+cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20221103172237-443f56ff4ba8 h1:d+pBUmsteW5tM87xmVXHZ4+LibHRFn40SPAoZJOg2ak=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
+github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=
+github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
+github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
+github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/distribution/distribution/v3 v3.0.0-20230621170613-87b280718d38 h1:zasJGKkPeS7071ifIgt0OVr7pShqedu5tRiAat8sWQg=
+github.com/distribution/distribution/v3 v3.0.0-20230621170613-87b280718d38/go.mod h1:+fqBJ4vPYo4Uu1ZE4d+bUtTLRXfdSL3NvCZIZ9GHv58=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4=
+github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d h1:2Ow4x25aoCCeuc77bpZKsQtgnVEr2A2qqg26VtjDgD0=
+github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d/go.mod h1:phb4Vcwy5vWTWputEGnbcsrrSNviOcQBj6Yz9PQGLwc=
+github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
+github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
+github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
+github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
+github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs=
+github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
+github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
+github.com/google/go-attestation v0.3.2/go.mod h1:N0ADdnY0cr7eLJyZ75o8kofGGTUF2XrZTJuTPo5acwk=
+github.com/google/go-attestation v0.4.3 h1:hHhPfym1TZm88L7sWmdc/moikHt80ls6mEiU+QvhRvk=
+github.com/google/go-attestation v0.4.3/go.mod h1:7L6MpeaeEmJVJHpr/5cCrOE0SjNA2aFLfJF1Og0AJS8=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI=
+github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw=
+github.com/google/go-tpm v0.3.2/go.mod h1:j71sMBTfp3X5jPHz852ZOfQMUOf65Gb/Th8pRmp7fvg=
+github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo=
+github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
+github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
+github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
+github.com/google/go-tpm-tools v0.2.1/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
+github.com/google/go-tpm-tools v0.3.1 h1:AFlmenDrIe0WU5AvpbfGFOLprTJTg/fCwmTyFdDEjbM=
+github.com/google/go-tpm-tools v0.3.1/go.mod h1:PSg+r5hSZI5tP3X7LBQx2sW1VSZUqZHBSrKyDqrB21U=
+github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad h1:LnpS22S8V1HqbxjveESGAazHhi6BX9SwI2Rij7qZcXQ=
+github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw=
+github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU=
+github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
+github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
+github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58 h1:e1AuZg7Lk0WSy8OiFaoLV+gXIUH1+Bg3tcYLLQJCZBs=
+github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58/go.mod h1:ngUsvRNfxdlJb0cHAlP6xDmCDJGJhXPKAH0ExDdDAU0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
+github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo=
+github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc=
+github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
+github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/exporter-toolkit v0.8.1 h1:TpKt8z55q1zF30BYaZKqh+bODY0WtByHDOhDA2M9pEs=
+github.com/prometheus/exporter-toolkit v0.8.1/go.mod h1:00shzmJL7KxcsabLWcONwpyNEuWhREOnFqZW7vadFS0=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA=
+github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q=
+github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
+github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So=
+github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
+github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
+github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k=
+go.etcd.io/etcd/api/v3 v3.5.5 h1:BX4JIbQ7hl7+jL+g+2j5UAr0o1bctCm6/Ct+ArBGkf0=
+go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
+go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8=
+go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
+go.etcd.io/etcd/client/v3 v3.5.5 h1:q++2WTJbUgpQu4B6hCuT7VkdwaTP7Qz6Daak3WzbrlI=
+go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
+go.fuhry.dev/fsnotify v1.7.2 h1:jPBFFKaJKUv8kSl66IHkkbBmV499/y1qqjHmecApbtg=
+go.fuhry.dev/fsnotify v1.7.2/go.mod h1:/nwTYd9m6GEeyIewNJFg3ikw+GsMe3EwpzlI5wB0Cz4=
+go.fuhry.dev/grpc-quic v0.1.2 h1:wJsr1rtkDxcX4fBaZDlRodBnvl2PIw9001EQR0zFbys=
+go.fuhry.dev/grpc-quic v0.1.2/go.mod h1:82aBkv1Q2C2sxlEY+MAaHFDmmrjl6K7660Ou4h+aJdw=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
+go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
+go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
+golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210316092937-0b90fd5c4c48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
+golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
+google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0=
+google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
+google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
+google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
+google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
+howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
--- /dev/null
+package acl
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/log"
+ "gopkg.in/yaml.v3"
+)
+
+// Interface ACLChecker
+type ACLChecker interface {
+ Check(string, *url.URL) error
+}
+
+type aclEntry struct {
+ User string `yaml:"user"`
+ Service string `yaml:"service"`
+ Domain string `yaml:"domain"`
+}
+
+// Type aclYaml defines the YAML structure for an ACL. It works as follows:
+//
+// identity.FooService:
+// - class: Domain
+// principal: .example.com
+// - class: User
+// principal: joe
+// - class: Service
+// principal: foo
+//
+// "identity" = mTLS identity the server is running as
+// "class" - one of the IdentityClass constants (Domain, User or Service)
+// "principal" - the thing to allow
+type aclYaml struct {
+ Services map[string][]*aclEntry `yaml:",inline"`
+}
+
+var aclSearchPaths = []string{
+ ".",
+ "/etc/runtime/grpc",
+}
+
+func TryLoadAcl(serverId mtls.Identity) ACLChecker {
+ logger := log.WithPrefix("ACLChecker")
+ for _, dir := range aclSearchPaths {
+ path := path.Join(dir, serverId.Name()+"_acl.yaml")
+ if ay, err := loadAclFromPath(path); err == nil {
+ logger.V(1).Infof("loaded ACLs from %s", path)
+ return ay
+ }
+ }
+
+ logger.V(1).Infof("using default ACLs for server %s", serverId.Name())
+ return &aclYaml{
+ Services: map[string][]*aclEntry{
+ "DEFAULT": {
+ {
+ Service: serverId.Name(),
+ },
+ },
+ },
+ }
+}
+
+func loadAclFromPath(path string) (*aclYaml, error) {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ ay := &aclYaml{}
+ err = yaml.Unmarshal(contents, ay)
+ if err != nil {
+ return nil, err
+ }
+
+ return ay, nil
+}
+
+func (ay *aclYaml) Check(method string, spiffe *url.URL) error {
+ logger := log.WithPrefix("ACLChecker")
+
+ slices := strings.Split(method, "/")
+ if len(slices) != 3 {
+ return fmt.Errorf("method name does not have exactly 3 components: %s [%d]", method, len(slices))
+ }
+
+ service, rpc := slices[1], slices[2]
+ var entries []*aclEntry
+ keys := []string{
+ service + "/" + rpc,
+ service,
+ "DEFAULT",
+ }
+ for _, k := range keys {
+ if e, ok := ay.Services[k]; ok {
+ entries = e
+ logger.V(2).Debugf("Using ACL entries from definition: %s", k)
+ break
+ }
+ }
+
+ if entries == nil {
+ return fmt.Errorf("method %q is not covered by acl, denying by default to client %s", method, spiffe)
+ }
+
+ for i, entry := range entries {
+ if entry.Service != "" && entry.User != "" {
+ return fmt.Errorf("ACLs: entry %d: error: service and user are mutually exclusive", i)
+ }
+
+ if entry.Domain != "" {
+ if strings.HasPrefix(entry.Domain, ".") && !strings.HasSuffix(spiffe.Host, entry.Domain) {
+ continue
+ }
+ if !strings.HasPrefix(entry.Domain, ".") && spiffe.Host != entry.Domain {
+ continue
+ }
+ }
+
+ if entry.Service == "*" && strings.HasPrefix(spiffe.Path, "/service/") {
+ // nothing, allow this case
+ } else if entry.Service != "" && spiffe.Path != fmt.Sprintf("/service/%s", entry.Service) {
+ continue
+ }
+
+ if entry.User == "*" && strings.HasPrefix(spiffe.Path, "/user/") {
+ // nothing, allow this case
+ } else if entry.User != "" && spiffe.Path != fmt.Sprintf("/user/%s", entry.User) {
+ continue
+ }
+
+ logger.V(2).Infof("accepting identity %s for method %q based on ACL entry %+v", spiffe, method, entry)
+ return nil
+ }
+
+ return fmt.Errorf("no matching entry found, denying method %s to client %s", method, spiffe)
+}
--- /dev/null
+package grpc
+
+import (
+ "context"
+ "fmt"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/sd"
+ "google.golang.org/grpc"
+)
+
+type Client struct {
+ ctx context.Context
+ serverId mtls.Identity
+ clientId mtls.Identity
+ watcher *sd.SDWatcher
+ connFac ConnectionFactory
+}
+
+func NewGrpcClient(ctx context.Context, serverId, clientId mtls.Identity) (*Client, error) {
+ etcdc, err := sd.NewDefaultEtcdClient()
+ if err != nil {
+ panic(err)
+ }
+
+ w := &sd.SDWatcher{
+ Service: serverId.Name(),
+ EtcdClient: etcdc,
+ Protocol: sd.ProtocolGRPC,
+ }
+
+ cl := &Client{
+ ctx: ctx,
+ serverId: serverId,
+ clientId: clientId,
+ watcher: w,
+ connFac: NewDefaultConnectionFactory(),
+ }
+
+ return cl, nil
+}
+
+func (c *Client) Conn() (*grpc.ClientConn, error) {
+ addrs, err := c.watcher.GetAddrs(c.ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ tc, err := c.clientId.TlsConfig(c.ctx)
+ if err != nil {
+ return nil, err
+ }
+ tc.ServerName = addrs[0].Hostname
+ verifier := mtls.NewPeerNameVerifier()
+ verifier.AllowFrom(mtls.Service, c.serverId.Name())
+ err = verifier.ConfigureClient(tc)
+ if err != nil {
+ return nil, err
+ }
+ creds := grpc.WithTransportCredentials(c.connFac.NewCredentials(tc))
+ dialer := grpc.WithContextDialer(c.connFac.NewDialer(tc))
+ opts := []grpc.DialOption{
+ creds,
+ dialer,
+ }
+
+ target := fmt.Sprintf("%s:%d", addrs[0].Hostname, addrs[0].Port)
+ conn, err := grpc.DialContext(c.ctx, target, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return conn, nil
+}
--- /dev/null
+package grpc
+
+import (
+ "context"
+ "crypto/tls"
+ "flag"
+ "net"
+
+ "go.fuhry.dev/runtime/utils/log"
+ "google.golang.org/grpc/credentials"
+)
+
+type createTransportFunc func() ConnectionFactory
+
+var supportedTransports map[string]createTransportFunc
+var defaultTransport string
+
+type ContextDialer func(ctx context.Context, address string) (net.Conn, error)
+
+type ConnectionFactory interface {
+ NewCredentials(tlsConfig *tls.Config) credentials.TransportCredentials
+ NewListener(port uint16, tlsConfig *tls.Config) (net.Listener, error)
+ NewDialer(tlsConfig *tls.Config) ContextDialer
+}
+
+func NewDefaultConnectionFactory() ConnectionFactory {
+ if !flag.Parsed() {
+ flag.Parse()
+ }
+
+ if createFunc, ok := supportedTransports[defaultTransport]; ok {
+ return createFunc()
+ }
+
+ log.Panicf("transport %q is not supported", defaultTransport)
+ panic("")
+}
+
+func init() {
+ flag.StringVar(&defaultTransport, "grpc.transport", "tcp", "transport for gRPC server and client (tcp or quic)")
+}
+
+func RegisterTransport(transportName string, createFunc createTransportFunc) {
+ if supportedTransports == nil {
+ supportedTransports = make(map[string]createTransportFunc, 0)
+ }
+
+ if _, ok := supportedTransports[transportName]; ok {
+ log.Panicf("transport %q is already registered", transportName)
+ }
+
+ supportedTransports[transportName] = createFunc
+}
--- /dev/null
+package grpc
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+
+ "google.golang.org/grpc/credentials"
+
+ "github.com/quic-go/quic-go"
+ grpc_quic "go.fuhry.dev/grpc-quic"
+)
+
+type QUICConnectionFactory struct{}
+
+func (cf *QUICConnectionFactory) NewCredentials(tlsConfig *tls.Config) credentials.TransportCredentials {
+ tlsConfig.NextProtos = []string{"grpc-quic"}
+ return grpc_quic.NewCredentials(tlsConfig)
+}
+
+func (cf *QUICConnectionFactory) NewListener(port uint16, tlsConfig *tls.Config) (net.Listener, error) {
+ udpListener, err := net.ListenPacket("udp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ return nil, err
+ }
+
+ quicListener, err := quic.Listen(udpListener, tlsConfig, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ listener := grpc_quic.Listen(*quicListener)
+
+ return listener, nil
+}
+
+func (cf *QUICConnectionFactory) NewDialer(tlsConfig *tls.Config) ContextDialer {
+ return grpc_quic.NewQuicDialer(tlsConfig)
+}
+
+func init() {
+ RegisterTransport("quic", func() ConnectionFactory { return &QUICConnectionFactory{} })
+}
--- /dev/null
+package grpc
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+
+ "google.golang.org/grpc/credentials"
+)
+
+type TCPConnectionFactory struct {
+ Dialer *net.Dialer
+
+ tlsConfig *tls.Config
+}
+
+func (cf *TCPConnectionFactory) NewCredentials(tlsConfig *tls.Config) credentials.TransportCredentials {
+ cf.ingestTlsConfig(tlsConfig)
+
+ return credentials.NewTLS(cf.tlsConfig)
+}
+
+func (cf *TCPConnectionFactory) NewListener(port uint16, tlsConfig *tls.Config) (net.Listener, error) {
+ listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ return nil, err
+ }
+ return listener, nil
+}
+
+func (cf *TCPConnectionFactory) NewDialer(tlsConfig *tls.Config) ContextDialer {
+ return func(ctx context.Context, address string) (net.Conn, error) {
+ if cf.Dialer == nil {
+ cf.Dialer = &net.Dialer{}
+ }
+ return cf.Dialer.DialContext(ctx, "tcp", address)
+ }
+}
+
+func (cf *TCPConnectionFactory) ingestTlsConfig(tlsConfig *tls.Config) {
+ if cf.tlsConfig != nil {
+ return
+ }
+
+ localTlsConfig := tlsConfig.Clone()
+ localTlsConfig.NextProtos = []string{"h2"}
+
+ cf.tlsConfig = localTlsConfig
+}
+
+func init() {
+ RegisterTransport("tcp", func() ConnectionFactory { return &TCPConnectionFactory{} })
+}
--- /dev/null
+package grpc
+
+import (
+ "context"
+ "fmt"
+
+ "go.fuhry.dev/runtime/utils/log"
+ "google.golang.org/grpc/peer"
+)
+
+type Session interface {
+ Get(key any) (value any, ok bool)
+ Set(key, value any)
+}
+
+type session struct {
+ storage map[any]any
+}
+
+type tSessionKey int
+
+const (
+ kSession tSessionKey = iota
+ kServer
+)
+
+func SessionFromContext(ctx context.Context) Session {
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return nil
+ }
+
+ if peerSpiffe, err := PeerIdentity(peer); err == nil {
+ sessionKey := fmt.Sprintf("%s:%s:%s", peerSpiffe.String(), peer.Addr.Network(), peer.Addr.String())
+ log.Default().V(3).Debugf("peer session key: %s", sessionKey)
+ server := ctx.Value(kServer).(*Server)
+ if session, ok := server.sessions.Get(sessionKey); ok {
+ return session
+ }
+
+ session := &session{
+ storage: make(map[any]any, 0),
+ }
+ server.sessions.Add(sessionKey, session)
+ return session
+ }
+
+ return nil
+}
+
+func (s *session) Get(key any) (value any, ok bool) {
+ value, ok = s.storage[key]
+ return
+}
+
+func (s *session) Set(key, value any) {
+ s.storage[key] = value
+}
--- /dev/null
+package grpc
+
+import (
+ "context"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "math/rand"
+ "net/url"
+
+ lru "github.com/hashicorp/golang-lru/v2"
+ grpc_quic "go.fuhry.dev/grpc-quic"
+ "go.fuhry.dev/runtime/grpc/acl"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/sd"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials"
+ "google.golang.org/grpc/peer"
+ "google.golang.org/grpc/status"
+)
+
+type Server struct {
+ grpcServer *grpc.Server
+ identity mtls.Identity
+ publisher *sd.SDPublisher
+ port uint16
+ verifier mtls.MTLSPeerVerifier
+ acl acl.ACLChecker
+ log *log.Logger
+ sessions *lru.Cache[string, *session]
+ connFac ConnectionFactory
+}
+
+var defaultPort *uint
+
+func RandomPort() uint {
+ return uint(1025 + (uint(rand.Int()) % (65535 - 1025)))
+}
+
+func NewGrpcServer(id mtls.Identity) (*Server, error) {
+ if !flag.Parsed() {
+ panic("cannot start grpc services before flags are parsed")
+ }
+
+ return NewGrpcServerWithPort(id, uint16(*defaultPort))
+}
+
+func NewGrpcServerWithPort(id mtls.Identity, port uint16) (*Server, error) {
+ etcdc, err := sd.NewDefaultEtcdClient()
+ if err != nil {
+ panic(err)
+ }
+
+ pub := &sd.SDPublisher{
+ Regions: []string{
+ hostname.RegionName(),
+ },
+ Service: id.Name(),
+ Protocol: sd.ProtocolGRPC,
+ EtcdClient: etcdc,
+ AdvertisePort: port,
+ }
+
+ // We want client certificate verification turned on, but no filtering on the
+ // client name to be done at handshake time - ACLs are enforced by handleConnection
+ // which calls out to acl_yaml.
+ cv := mtls.NewPeerNameVerifier()
+ cv.AllowFrom(mtls.All)
+
+ sessionsLru, err := lru.New[string, *session](1024)
+ if err != nil {
+ return nil, err
+ }
+
+ server := &Server{
+ identity: id,
+ publisher: pub,
+ port: port,
+ acl: acl.TryLoadAcl(id),
+ verifier: cv,
+ log: log.WithPrefix(fmt.Sprintf("grpcServer:%s", id.Name())),
+ sessions: sessionsLru,
+ connFac: NewDefaultConnectionFactory(),
+ }
+
+ return server, nil
+}
+
+func (s *Server) PublishAndServe(ctx context.Context, callback func(*grpc.Server)) error {
+ s.log.Noticef("starting %s service on port %d", s.identity.Name(), s.port)
+
+ tc, err := s.identity.TlsConfig(ctx)
+ if err != nil {
+ return err
+ }
+ tc.MinVersion = tls.VersionTLS13
+ tc.MaxVersion = tls.VersionTLS13
+
+ err = s.verifier.ConfigureServer(tc)
+ if err != nil {
+ return err
+ }
+
+ opts := make([]grpc.ServerOption, 0)
+
+ txCreds := s.connFac.NewCredentials(tc)
+ listener, err := s.connFac.NewListener(s.port, tc)
+ if err != nil {
+ return err
+ }
+ creds := grpc.Creds(txCreds)
+ opts = append(opts, creds)
+
+ usi := grpc.ChainUnaryInterceptor(s.handleConnection)
+ opts = append(opts, usi)
+
+ ssi := grpc.ChainStreamInterceptor(s.handleStreamConnection)
+ opts = append(opts, ssi)
+
+ grpcServer := grpc.NewServer(opts...)
+
+ callback(grpcServer)
+
+ go grpcServer.Serve(listener)
+ s.grpcServer = grpcServer
+
+ err = s.publisher.Publish(ctx)
+ if err != nil {
+ grpcServer.Stop()
+ return err
+ }
+
+ s.log.Infof("%s server started", s.identity.Name())
+
+ return nil
+}
+
+func (s *Server) Stop() {
+ s.publisher.Unpublish()
+ if s.grpcServer != nil {
+ s.grpcServer.GracefulStop()
+ s.grpcServer = nil
+ }
+}
+
+func (s *Server) handleConnection(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return nil, status.Errorf(codes.PermissionDenied, "client did not authenticate")
+ }
+ spiffe, err := PeerIdentity(peer)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := s.acl.Check(info.FullMethod, spiffe); err != nil {
+ return nil, status.Errorf(codes.PermissionDenied, err.Error())
+ }
+
+ serverCtx := context.WithValue(ctx, kServer, s)
+
+ return handler(serverCtx, req)
+}
+
+func (s *Server) handleStreamConnection(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
+ ctx := ss.Context()
+ peer, ok := peer.FromContext(ctx)
+ if !ok {
+ return status.Errorf(codes.PermissionDenied, "client did not authenticate")
+ }
+ spiffe, err := PeerIdentity(peer)
+ if err != nil {
+ return err
+ }
+
+ if err := s.acl.Check(info.FullMethod, spiffe); err != nil {
+ return status.Errorf(codes.PermissionDenied, err.Error())
+ }
+
+ return handler(srv, ss)
+}
+
+func PeerIdentity(peer *peer.Peer) (*url.URL, error) {
+ if peer.AuthInfo == nil {
+ return nil, status.Errorf(codes.PermissionDenied, "no AuthInfo present in peer information")
+ }
+
+ var tlsState tls.ConnectionState
+
+ switch ai := peer.AuthInfo.(type) {
+ case credentials.TLSInfo:
+ if ai.SPIFFEID != nil {
+ return ai.SPIFFEID, nil
+ }
+
+ tlsState = ai.State
+ case *grpc_quic.Info:
+ conn := ai.Conn()
+ tlsState = conn.(*grpc_quic.Conn).TLSState()
+ default:
+ return nil, status.Errorf(codes.PermissionDenied, "unhandled type of peer.AuthInfo: %T", peer.AuthInfo)
+ }
+
+ if len(tlsState.PeerCertificates) > 0 {
+ cert := tlsState.PeerCertificates[0]
+ if url := certutil.SpiffeUrlFromCertificate(cert); url != nil {
+ return url, nil
+ }
+ }
+
+ return nil, status.Errorf(codes.PermissionDenied, "could not determine your SPIFFEID from your certificate")
+}
+
+func init() {
+ defaultPort = flag.Uint("grpc.port", RandomPort(), "port for server to listen on")
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ exporter "go.fuhry.dev/runtime/ldap"
+ "go.fuhry.dev/runtime/mtls"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v2/altsrc"
+ "golang.org/x/sync/errgroup"
+)
+
+const (
+ promAddr = "promAddr"
+ ldapNet = "ldapNet"
+ ldapAddr = "ldapAddr"
+ ldapUser = "ldapUser"
+ ldapPass = "ldapPass"
+ mtlsId = "mtlsId"
+ interval = "interval"
+ metrics = "metrPath"
+ jsonLog = "jsonLog"
+ webCfgFile = "webCfgFile"
+ config = "config"
+ replicationObject = "replicationObject"
+)
+
+func main() {
+ flags := []cli.Flag{
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: promAddr,
+ Value: ":9330",
+ Usage: "Bind address for Prometheus HTTP metrics server",
+ EnvVars: []string{"PROM_ADDR"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: metrics,
+ Value: "/metrics",
+ Usage: "Path on which to expose Prometheus metrics",
+ EnvVars: []string{"METRICS_PATH"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: ldapNet,
+ Value: "tcp",
+ Usage: "Network of OpenLDAP server",
+ EnvVars: []string{"LDAP_NET"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: ldapAddr,
+ Value: "localhost:389",
+ Usage: "Address and port of OpenLDAP server",
+ EnvVars: []string{"LDAP_ADDR"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: ldapUser,
+ Usage: "OpenLDAP bind username (optional)",
+ EnvVars: []string{"LDAP_USER"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: ldapPass,
+ Usage: "OpenLDAP bind password (optional)",
+ EnvVars: []string{"LDAP_PASS"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: mtlsId,
+ Usage: "mTLS identity from /etc/ssl/mtls",
+ EnvVars: []string{"MTLS_ID"},
+ }),
+ altsrc.NewDurationFlag(&cli.DurationFlag{
+ Name: interval,
+ Value: 30 * time.Second,
+ Usage: "Scrape interval",
+ EnvVars: []string{"INTERVAL"},
+ }),
+ altsrc.NewStringFlag(&cli.StringFlag{
+ Name: webCfgFile,
+ Usage: "Prometheus metrics web config `FILE` (optional)",
+ EnvVars: []string{"WEB_CFG_FILE"},
+ }),
+ altsrc.NewBoolFlag(&cli.BoolFlag{
+ Name: jsonLog,
+ Value: false,
+ Usage: "Output logs in JSON format",
+ EnvVars: []string{"JSON_LOG"},
+ }),
+ altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
+ Name: replicationObject,
+ Usage: "Object to watch replication upon",
+ }),
+ &cli.StringFlag{
+ Name: config,
+ Usage: "Optional configuration from a `YAML_FILE`",
+ },
+ }
+ app := &cli.App{
+ Name: "openldap_exporter",
+ Usage: "Export OpenLDAP metrics to Prometheus",
+ Before: altsrc.InitInputSourceWithContext(flags, optionalYamlSourceFunc(config)),
+ Version: exporter.GetVersion(),
+ HideHelpCommand: true,
+ Flags: flags,
+ Action: runMain,
+ }
+ if err := app.Run(os.Args); err != nil {
+ log.WithError(err).Fatal("service failed")
+ }
+ log.Info("service stopped")
+}
+
+func optionalYamlSourceFunc(flagFileName string) func(context *cli.Context) (altsrc.InputSourceContext, error) {
+ return func(c *cli.Context) (altsrc.InputSourceContext, error) {
+ filePath := c.String(flagFileName)
+ if filePath != "" {
+ return altsrc.NewYamlSourceFromFile(filePath)
+ }
+ return &altsrc.MapInputSource{}, nil
+ }
+}
+
+func runMain(c *cli.Context) error {
+ if c.Bool(jsonLog) {
+ log.SetFormatter(&log.JSONFormatter{})
+ } else {
+ log.SetFormatter(&log.TextFormatter{})
+ }
+ log.Info("service starting")
+
+ server := exporter.NewMetricsServer(
+ c.String(promAddr),
+ c.String(metrics),
+ c.String(webCfgFile),
+ )
+
+ scraper := &exporter.Scraper{
+ Net: c.String(ldapNet),
+ Addr: c.String(ldapAddr),
+ User: c.String(ldapUser),
+ Pass: c.String(ldapPass),
+ Mtls: mtls.NewServiceIdentity(c.String(mtlsId)),
+ Tick: c.Duration(interval),
+ Sync: c.StringSlice(replicationObject),
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ var group errgroup.Group
+ group.Go(func() error {
+ defer cancel()
+ return server.Start()
+ })
+ group.Go(func() error {
+ defer cancel()
+ scraper.Start(ctx)
+ return nil
+ })
+ group.Go(func() error {
+ defer func() {
+ cancel()
+ server.Stop()
+ }()
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
+ select {
+ case <-signalChan:
+ log.Info("shutdown received")
+ return nil
+ case <-ctx.Done():
+ return nil
+ }
+ })
+ return group.Wait()
+}
--- /dev/null
+[Unit]
+Description=Monitor LDAP server health on %i
+Wants=systemd-networkd-wait-online.service
+After=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+User=slapmon
+ExecStart=/usr/bin/ldap-health-exporter --webCfgFile /etc/prometheus-node-exporter/web-config.yml --ldapAddr %i:636 --mtlsId slapmon --interval=30s
+
+[Install]
+Alias=ldap-health-exporter@%i.service
+WantedBy=multi-user.target
+
--- /dev/null
+package openldap_exporter
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ ldap "github.com/go-ldap/ldap/v3"
+ "github.com/prometheus/client_golang/prometheus"
+ log "github.com/sirupsen/logrus"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/mtls/certutil"
+)
+
+const (
+ baseDN = "cn=Monitor"
+ opsBaseDN = "cn=Operations,cn=Monitor"
+
+ monitorCounterObject = "monitorCounterObject"
+ monitorCounter = "monitorCounter"
+
+ monitoredObject = "monitoredObject"
+ monitoredInfo = "monitoredInfo"
+
+ monitorOperation = "monitorOperation"
+ monitorOpCompleted = "monitorOpCompleted"
+
+ monitorReplicationFilter = "contextCSN"
+ monitorReplication = "monitorReplication"
+
+ threadsBaseDN = "cn=Threads,cn=Monitor"
+)
+
+type query struct {
+ baseDN string
+ searchFilter string
+ searchAttr string
+ metric prometheus.Collector
+ setData func([]*ldap.Entry, *query)
+}
+
+var (
+ monitoredObjectCounter = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Subsystem: "openldap",
+ Name: "monitored_object",
+ Help: help(baseDN, objectClass(monitoredObject), monitoredInfo),
+ },
+ []string{"dn"},
+ )
+ monitorCounterObjectGauge = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Subsystem: "openldap",
+ Name: "monitor_counter_object",
+ Help: help(baseDN, objectClass(monitorCounterObject), monitorCounter),
+ },
+ []string{"dn"},
+ )
+ monitorOperationCounter = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Subsystem: "openldap",
+ Name: "monitor_operation",
+ Help: help(opsBaseDN, objectClass(monitorOperation), monitorOpCompleted),
+ },
+ []string{"dn"},
+ )
+ bindCounter = prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Subsystem: "openldap",
+ Name: "bind",
+ Help: "successful vs unsuccessful ldap bind attempts",
+ },
+ []string{"result"},
+ )
+ dialCounter = prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Subsystem: "openldap",
+ Name: "dial",
+ Help: "successful vs unsuccessful ldap dial attempts",
+ },
+ []string{"result"},
+ )
+ scrapeCounter = prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Subsystem: "openldap",
+ Name: "scrape",
+ Help: "successful vs unsuccessful ldap scrape attempts",
+ },
+ []string{"result"},
+ )
+ monitorReplicationGauge = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Subsystem: "openldap",
+ Name: "monitor_replication",
+ Help: help(baseDN, monitorReplication),
+ },
+ []string{"id", "type"},
+ )
+ queries = []*query{
+ {
+ baseDN: baseDN,
+ searchFilter: objectClass(monitoredObject),
+ searchAttr: monitoredInfo,
+ metric: monitoredObjectCounter,
+ setData: setValue,
+ },
+ {
+ baseDN: baseDN,
+ searchFilter: objectClass(monitorCounterObject),
+ searchAttr: monitorCounter,
+ metric: monitorCounterObjectGauge,
+ setData: setValue,
+ },
+ {
+ baseDN: opsBaseDN,
+ searchFilter: objectClass(monitorOperation),
+ searchAttr: monitorOpCompleted,
+ metric: monitorOperationCounter,
+ setData: setValue,
+ },
+ {
+ baseDN: opsBaseDN,
+ searchFilter: objectClass(monitorOperation),
+ searchAttr: monitorOpCompleted,
+ metric: monitorOperationCounter,
+ setData: setValue,
+ },
+ }
+)
+
+func init() {
+ prometheus.MustRegister(
+ monitoredObjectCounter,
+ monitorCounterObjectGauge,
+ monitorOperationCounter,
+ monitorReplicationGauge,
+ scrapeCounter,
+ bindCounter,
+ dialCounter,
+ )
+}
+
+func help(msg ...string) string {
+ return strings.Join(msg, " ")
+}
+
+func objectClass(name string) string {
+ return fmt.Sprintf("(objectClass=%v)", name)
+}
+
+func setValue(entries []*ldap.Entry, q *query) {
+ for _, entry := range entries {
+ val := entry.GetAttributeValue(q.searchAttr)
+ if val == "" {
+ // not every entry will have this attribute
+ continue
+ }
+ num, err := strconv.ParseFloat(val, 64)
+ if err != nil {
+ // some of these attributes are not numbers
+ continue
+ }
+ if gauge, ok := q.metric.(*prometheus.GaugeVec); ok {
+ gauge.WithLabelValues(entry.DN).Set(num)
+ } else if counter, ok := q.metric.(*prometheus.CounterVec); ok {
+ counter.WithLabelValues(entry.DN).Add(num)
+ }
+ }
+}
+
+func setReplicationValue(entries []*ldap.Entry, q *query) {
+ for _, entry := range entries {
+ val := entry.GetAttributeValue(q.searchAttr)
+ if val == "" {
+ // not every entry will have this attribute
+ continue
+ }
+ fields := log.Fields{
+ "filter": q.searchFilter,
+ "attr": q.searchAttr,
+ "value": val,
+ }
+ valueBuffer := strings.Split(val, "#")
+ gt, err := time.Parse("20060102150405.999999Z", valueBuffer[0])
+ if err != nil {
+ log.WithFields(fields).WithError(err).Warn("unexpected gt value")
+ continue
+ }
+ count, err := strconv.ParseFloat(valueBuffer[1], 64)
+ if err != nil {
+ log.WithFields(fields).WithError(err).Warn("unexpected count value")
+ continue
+ }
+ sid := valueBuffer[2]
+ mod, err := strconv.ParseFloat(valueBuffer[3], 64)
+ if err != nil {
+ log.WithFields(fields).WithError(err).Warn("unexpected mod value")
+ continue
+ }
+ if gauge, ok := q.metric.(*prometheus.GaugeVec); ok {
+ gauge.WithLabelValues(sid, "gt").Set(float64(gt.Unix()))
+ gauge.WithLabelValues(sid, "count").Set(count)
+ gauge.WithLabelValues(sid, "mod").Set(mod)
+ } else if counter, ok := q.metric.(*prometheus.CounterVec); ok {
+ counter.WithLabelValues(sid, "gt").Add(float64(gt.Unix()))
+ counter.WithLabelValues(sid, "count").Add(count)
+ counter.WithLabelValues(sid, "mod").Add(mod)
+ }
+ }
+}
+
+type Scraper struct {
+ Net string
+ Addr string
+ User string
+ Pass string
+ Mtls mtls.Identity
+ Tick time.Duration
+ LdapSync []string
+ log log.FieldLogger
+ Sync []string
+
+ tlsConfig *tls.Config
+}
+
+func (s *Scraper) addReplicationQueries() {
+ for _, q := range s.Sync {
+ queries = append(queries,
+ &query{
+ baseDN: q,
+ searchFilter: objectClass("*"),
+ searchAttr: monitorReplicationFilter,
+ metric: monitorReplicationGauge,
+ setData: setReplicationValue,
+ },
+ )
+ }
+}
+
+func (s *Scraper) Start(ctx context.Context) {
+ s.log = log.WithField("component", "scraper")
+ s.setTlsConfig(ctx)
+ s.addReplicationQueries()
+ address := fmt.Sprintf("%s://%s", s.Net, s.Addr)
+ s.log.WithField("addr", address).Info("starting monitor loop")
+ ticker := time.NewTicker(s.Tick)
+ defer ticker.Stop()
+ s.scrape()
+ for {
+ select {
+ case <-ticker.C:
+ s.scrape()
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func (s *Scraper) setTlsConfig(ctx context.Context) {
+ var tlsConfig *tls.Config
+ if !s.Mtls.IsValid() {
+ s.log.Error(fmt.Sprintf("mtls identity %q is invalid", s.Mtls.Name()))
+ return
+ }
+ tlsConfig, err := s.Mtls.TlsConfig(ctx)
+ if err != nil {
+ s.log.WithError(err).Error("generating tls config failed")
+ return
+ }
+ if tlsConfig.RootCAs != nil {
+ systemCertificates, err := certutil.LoadCertificatesFromPEM("/etc/ssl/certs/ca-certificates.crt")
+ if err != nil {
+ s.log.WithError(err).Error("loading system CA certificate store failed")
+ return
+ }
+ for _, cert := range systemCertificates {
+ tlsConfig.RootCAs.AddCert(cert)
+ }
+ }
+
+ s.tlsConfig = tlsConfig
+}
+
+func (s *Scraper) scrape() {
+ var conn *ldap.Conn
+ var err error
+ if s.tlsConfig != nil {
+ conn, err = ldap.DialTLS(s.Net, s.Addr, s.tlsConfig)
+ } else {
+ conn, err = ldap.Dial(s.Net, s.Addr)
+ }
+ if err != nil {
+ s.log.WithError(err).Error("dial failed")
+ dialCounter.WithLabelValues("fail").Inc()
+ return
+ }
+ dialCounter.WithLabelValues("ok").Inc()
+ defer (func() {
+ if conn != nil {
+ conn.Close()
+ }
+ })()
+
+ if s.Mtls.Name() != "" {
+ err = conn.ExternalBind()
+ if err != nil {
+ s.log.WithError(err).Error("external bind failed")
+ dialCounter.WithLabelValues("fail").Inc()
+ return
+ }
+ bindCounter.WithLabelValues("ok").Inc()
+ } else if s.User != "" && s.Pass != "" {
+ err = conn.Bind(s.User, s.Pass)
+ if err != nil {
+ s.log.WithError(err).Error("bind failed")
+ bindCounter.WithLabelValues("fail").Inc()
+ return
+ }
+ bindCounter.WithLabelValues("ok").Inc()
+ }
+
+ scrapeRes := "ok"
+ for _, q := range queries {
+ if err = scrapeQuery(conn, q); err != nil {
+ s.log.WithError(err).WithField("filter", q.searchFilter).Warn("query failed")
+ scrapeRes = "fail"
+ }
+ }
+ scrapeCounter.WithLabelValues(scrapeRes).Inc()
+}
+
+func scrapeQuery(conn *ldap.Conn, q *query) error {
+ req := ldap.NewSearchRequest(
+ q.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+ q.searchFilter, []string{q.searchAttr}, nil,
+ )
+ sr, err := conn.Search(req)
+ if err != nil {
+ return err
+ }
+ q.setData(sr.Entries, q)
+ return nil
+}
--- /dev/null
+package openldap_exporter
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ kitlog "github.com/go-kit/log"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/prometheus/exporter-toolkit/web"
+ log "github.com/sirupsen/logrus"
+)
+
+var commit string
+var tag string
+
+func GetVersion() string {
+ return fmt.Sprintf("%s (%s)", tag, commit)
+}
+
+type Server struct {
+ server *http.Server
+ flagConfig *web.FlagConfig
+ logger log.FieldLogger
+}
+
+func NewMetricsServer(bindAddr, metricsPath, tlsConfigPath string) *Server {
+ mux := http.NewServeMux()
+ mux.Handle(metricsPath, promhttp.Handler())
+ mux.HandleFunc("/version", showVersion)
+ systemdSocket := false
+ return &Server{
+ server: &http.Server{Addr: bindAddr, Handler: mux},
+ flagConfig: &web.FlagConfig{
+ WebListenAddresses: &[]string{bindAddr},
+ WebSystemdSocket: &systemdSocket,
+ WebConfigFile: &tlsConfigPath,
+ },
+ logger: log.WithField("component", "server"),
+ }
+}
+
+func showVersion(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ fmt.Fprintln(w, GetVersion())
+}
+
+func (s *Server) Start() error {
+ s.logger.WithField("addr", s.server.Addr).Info("starting http listener")
+ err := web.ListenAndServe(s.server, s.flagConfig, kitlog.LoggerFunc(s.adaptor))
+ if errors.Is(err, http.ErrServerClosed) {
+ return nil
+ }
+ return err
+}
+
+func (s *Server) Stop() {
+ ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+ s.server.Shutdown(ctx)
+ cancel()
+}
+
+func (s *Server) adaptor(kvs ...interface{}) error {
+ if len(kvs) == 0 {
+ return nil
+ }
+ if len(kvs)%2 != 0 {
+ kvs = append(kvs, nil)
+ }
+ fields := log.Fields{}
+ for i := 0; i < len(kvs); i += 2 {
+ key := fmt.Sprint(kvs[i])
+ fields[key] = kvs[i+1]
+ }
+ var msg string
+ if val, ok := fields["msg"]; ok {
+ delete(fields, "msg")
+ msg = fmt.Sprint(val)
+ }
+ var level string
+ if val, ok := fields["level"]; ok {
+ delete(fields, "level")
+ level = fmt.Sprint(val)
+ }
+ ll := s.logger.WithFields(fields)
+ switch level {
+ case "error":
+ ll.Error(msg)
+ case "warn":
+ ll.Warn(msg)
+ case "debug":
+ ll.Debug(msg)
+ default:
+ ll.Info(msg)
+ }
+ return nil
+}
--- /dev/null
+package machines
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+var (
+ defaultMachinesApiUrl = "https://" + constants.MachinesHost + "/api/"
+ defaultMachinesEventsUrl = "wss://" + constants.MachinesHost + "/events"
+ defaultMachinesApiScopes = "read"
+ machinesApiUrl, machinesEventsUrl string
+ machinesOauthClientId, machinesOauthClientSecret string
+ machinesApiScopes string
+)
+
+type MachinesClient interface {
+ APICall(route string, data interface{}, response any) error
+ NewEventListener(ctx context.Context) (chan MachinesMqttEvent, error)
+}
+
+type machinesClient struct {
+ MachinesClient
+
+ client *http.Client
+ baseUrl string
+ eventsUrl string
+ logger *log.Logger
+}
+
+func init() {
+ flag.StringVar(&machinesApiUrl, "machines.api-url", defaultMachinesApiUrl, "URL to the Machines API")
+ flag.StringVar(&machinesEventsUrl, "machines.events-url", defaultMachinesEventsUrl, "Machines MQTT WebSocket URL")
+ flag.StringVar(&machinesOauthClientId, "machines.client-id", "", "OAuth client ID for the Machines API")
+ flag.StringVar(&machinesOauthClientSecret, "machines.client-secret", "", "Client secret for the Machines API")
+ flag.StringVar(&machinesApiScopes, "machines.scopes", defaultMachinesApiScopes, "comma-separated list of OAuth scopes for the Machines API")
+}
+
+func NewDefaultMachinesClient(scopes ...string) (*machinesClient, error) {
+ if !flag.Parsed() {
+ return nil, errors.New("flags have not been parsed yet")
+ }
+
+ if len(scopes) == 0 {
+ scopes = strings.Split(machinesApiScopes, ",")
+ }
+
+ if machinesOauthClientId == "" || machinesOauthClientSecret == "" {
+ return nil, errors.New("cannot create default machines client: client id or client secret not provided on command line")
+ }
+
+ return NewMachinesClient(machinesApiUrl, machinesEventsUrl, machinesOauthClientId, machinesOauthClientSecret, scopes)
+}
+
+func NewMachinesClient(apiUrl, eventsUrl, clientId, clientSecret string, scopes []string) (*machinesClient, error) {
+ httpClient := &http.Client{}
+ apiUrl = strings.TrimRight(apiUrl, "/")
+ err := SetupOAuthClient(httpClient,
+ fmt.Sprintf("%s/oauth/token", apiUrl),
+ clientId, clientSecret, scopes)
+
+ if err != nil {
+ return nil, err
+ }
+
+ mc := &machinesClient{
+ client: httpClient,
+ baseUrl: apiUrl,
+ eventsUrl: eventsUrl,
+ logger: log.WithPrefix("MachinesClient"),
+ }
+
+ return mc, nil
+}
+
+func (mc *machinesClient) APICall(route string, data interface{}, response any) error {
+ var body io.Reader
+
+ method := "GET"
+ if data != nil {
+ method = "POST"
+ switch td := data.(type) {
+ case string:
+ body = bytes.NewReader([]byte(td))
+ case []byte:
+ body = bytes.NewReader(td)
+ default:
+ bodyJson, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ return err
+ }
+ body = bytes.NewReader(bodyJson)
+ }
+ }
+
+ route = strings.TrimLeft(route, "/")
+
+ req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", mc.baseUrl, route), body)
+ if err != nil {
+ return err
+ }
+
+ if body != nil {
+ req.Header.Set("content-type", "application/json")
+ }
+
+ resp, err := mc.client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return NewInvalidHttpResponseError(resp)
+ }
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(respBody, response)
+ if err != nil {
+ return fmt.Errorf("error decoding JSON body:\n=====BEGIN JSON=====\n%s\n=====END JSON=====\nerror: %v", respBody, err)
+ }
+
+ return nil
+}
+
+func NewInvalidHttpResponseError(resp *http.Response) error {
+ var bodyStr string
+ body, err := io.ReadAll(resp.Body)
+ if err == nil {
+ bodyStr = string(body)
+ } else {
+ bodyStr = fmt.Sprintf("[error reading body: %v]", err)
+ }
+
+ return fmt.Errorf("received invalid HTTP response with status %d (%s): %s", resp.StatusCode, resp.Status, bodyStr)
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "go.fuhry.dev/runtime/machines"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+ flag.Parse()
+ logger := log.WithPrefix("main")
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ client, err := machines.NewDefaultMachinesClient("host.attestation.client")
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ ectx, _ := context.WithCancel(ctx)
+
+ eventsChan, err := client.NewEventListener(ectx)
+
+ if err != nil {
+ logger.Panic(err)
+ }
+
+ logger.Info("successfully created event monitor, listening for events")
+mainLoop:
+ for {
+ select {
+ case evt := <-eventsChan:
+ logger.Noticef("got event with thing=%s, action=%s, tags=<%+v>\n", evt.ItemType, evt.Event, evt.Tags)
+ case <-ctx.Done():
+ break mainLoop
+ }
+ }
+
+ os.Exit(0)
+}
--- /dev/null
+package machines
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/url"
+
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type MachinesMqttEvent struct {
+ ItemType string `json:"thing"`
+ Event string `json:"event"`
+ Tags map[string]string `json:"tags"`
+}
+
+func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMqttEvent, error) {
+ mqttLogger := mc.logger.AppendPrefix(".mqtt")
+
+ mqtt.DEBUG = mqttLogger.WithLevel(log.DEBUG).V(3)
+ mqtt.WARN = mqttLogger.WithLevel(log.WARNING)
+ mqtt.ERROR = mqttLogger.WithLevel(log.ERROR)
+ mqtt.CRITICAL = mqttLogger.WithLevel(log.CRITICAL)
+
+ oauthClient := mc.client.Transport.(*oauthClient)
+ mqttOpts := mqtt.NewClientOptions()
+
+ mqttOpts.Order = false
+ mc.logger.V(1).Infof("attempting to connect to Machines MQTT server at %s", mc.eventsUrl)
+ eventsUrl, err := url.Parse(mc.eventsUrl)
+ if err != nil {
+ return nil, fmt.Errorf("events url %q is invalid: %v", mc.eventsUrl, err)
+ }
+
+ updateCreds := func(mqttOpts *mqtt.ClientOptions) error {
+ accessToken, err := oauthClient.getAccessToken()
+ if err != nil {
+ return err
+ }
+
+ mc.logger.V(2).Debugf("setting username/password to %s/%s", log.Redact(accessToken), "x")
+ mqttOpts.Username = accessToken
+ mqttOpts.Password = "x"
+
+ return nil
+ }
+
+ if err = updateCreds(mqttOpts); err != nil {
+ return nil, err
+ }
+
+ mqttOpts.Servers = append(mqttOpts.Servers, eventsUrl)
+
+ mqttOpts.OnReconnecting = func(client mqtt.Client, opts *mqtt.ClientOptions) {
+ updateCreds(opts)
+ }
+
+ client := mqtt.NewClient(mqttOpts)
+ t := client.Connect()
+
+ select {
+ case <-t.Done():
+ if err = t.Error(); err != nil {
+ return nil, err
+ }
+ case <-ctx.Done():
+ return nil, context.Canceled
+ }
+
+ if !client.IsConnected() {
+ return nil, errors.New("somehow we are still not connected??")
+ }
+
+ msgChan := make(chan MachinesMqttEvent)
+
+ handler := func(client mqtt.Client, msg mqtt.Message) {
+ msg.Ack()
+
+ mc.logger.V(2).Debugf("got raw mqtt msg: %s", msg.Payload())
+
+ obj := MachinesMqttEvent{}
+ if err := json.Unmarshal(msg.Payload(), &obj); err == nil {
+ msgChan <- obj
+ }
+ }
+
+ t = client.Subscribe(constants.MachinesMqttTopic, byte(0), handler)
+ select {
+ case <-t.Done():
+ break
+ case <-ctx.Done():
+ close(msgChan)
+ return nil, context.Canceled
+ }
+
+ go (func() {
+ <-ctx.Done()
+
+ t := client.Unsubscribe(constants.MachinesMqttTopic)
+ t.Wait()
+
+ close(msgChan)
+ client.Disconnect(0)
+ })()
+
+ return msgChan, nil
+}
--- /dev/null
+package machines
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type oauthClientCredentials_AccessTokenRequest struct {
+ GrantType string `json:"grant_type"`
+ Scope string `json:"scope"`
+ ClientId string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+}
+
+type oauthClientCredentials_AccessTokenReply struct {
+ TokenType string `json:"token_type"`
+ AccessToken string `json:"access_token"`
+ ClientId string `json:"client_id"`
+ ExpiresIn int64 `json:"expires_in"`
+ Scope string `json:"scope"`
+}
+
+type oauthClientCredentials_Store struct {
+ TokenEndpoint string `json:"token_endpoint"`
+ ExpiresAt int64 `json:"expires_at"`
+ Token oauthClientCredentials_AccessTokenReply `json:"token"`
+}
+
+type oauthAuthParams struct {
+ tokenEndpoint string
+ clientId string
+ clientSecret string
+ scope *hashset.HashSet[string]
+}
+
+type oauthClient struct {
+ *http.Transport
+
+ client *http.Client
+ store *oauthClientCredentials_Store
+ params *oauthAuthParams
+ log *log.Logger
+}
+
+func SetupOAuthClient(client *http.Client, tokenEndpoint string, clientId string, clientSecret string, scope []string) error {
+ clientCopy := *client
+ client.Transport = &oauthClient{
+ client: &clientCopy,
+ params: &oauthAuthParams{
+ tokenEndpoint: tokenEndpoint,
+ clientId: clientId,
+ clientSecret: clientSecret,
+ scope: hashset.FromSlice(scope),
+ },
+ log: log.WithPrefix("oauth2"),
+ }
+
+ return nil
+}
+
+func (oc *oauthClient) RoundTrip(request *http.Request) (*http.Response, error) {
+ accessToken, err := oc.getAccessToken()
+ if err != nil {
+ oc.log.Errorf("access token retrieval failed, because: %v\n", err)
+ return nil, err
+ }
+
+ request.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
+
+ return oc.client.Do(request)
+}
+
+func (oc *oauthClient) credentialCachePath() string {
+ return path.Join(os.TempDir(), fmt.Sprintf("machines-auth-%d.json", os.Getuid()))
+}
+
+func (oc *oauthClient) getAccessToken() (string, error) {
+ accessToken, err := oc.loadAccessToken()
+ if err != nil {
+ oc.log.V(1).Infof("getting a new access token, because: %v\n", err)
+ accessToken, err = oc.renewAccessToken()
+ if err != nil {
+ oc.log.Criticalf("access token retrieval failed, because: %v\n", err)
+ return "", err
+ }
+ }
+
+ return accessToken, nil
+}
+
+func (oc *oauthClient) loadAccessToken() (string, error) {
+ now := time.Now()
+
+ // if the client's store in memory has an expired access token, reread it from
+ // the disk
+ if oc.store != nil {
+ if oc.store.ExpiresAt >= now.Unix() {
+ oc.store = nil
+ }
+ }
+ if oc.store == nil {
+ err := oc.readCredentialCacheFromDisk()
+ if err != nil {
+ return "", err
+ }
+ }
+
+ return oc.store.Token.AccessToken, nil
+}
+
+func (oc *oauthClient) renewAccessToken() (string, error) {
+ req := oauthClientCredentials_AccessTokenRequest{
+ GrantType: "client_credentials",
+ ClientId: oc.params.clientId,
+ ClientSecret: oc.params.clientSecret,
+ Scope: strings.Join(oc.params.scope.AsSlice(), " "),
+ }
+
+ reqBody, err := json.Marshal(req)
+ if err != nil {
+ return "", err
+ }
+
+ reader := bytes.NewReader(reqBody)
+ httpReq, err := http.NewRequest("POST", oc.params.tokenEndpoint, reader)
+ if err != nil {
+ return "", err
+ }
+
+ httpReq.Header.Set("content-type", "application/json")
+
+ response, err := oc.client.Do(httpReq)
+ if err != nil {
+ return "", err
+ }
+
+ if response.StatusCode != http.StatusOK {
+ return "", NewInvalidHttpResponseError(response)
+ }
+
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return "", err
+ }
+
+ resp := &oauthClientCredentials_AccessTokenReply{}
+ err = json.Unmarshal(body, resp)
+ if err != nil {
+ return "", err
+ }
+
+ err = oc.storeCredentials(resp)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.AccessToken, nil
+}
+
+func (oc *oauthClient) readCredentialCacheFromDisk() error {
+ ccPath := oc.credentialCachePath()
+
+ contents, err := os.ReadFile(ccPath)
+ if err != nil {
+ return err
+ }
+
+ store := oauthClientCredentials_Store{}
+
+ err = json.Unmarshal(contents, &store)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now()
+ if store.ExpiresAt <= now.Unix() {
+ return errors.New("access token has expired")
+ }
+
+ tokenScope := hashset.FromSlice(strings.Split(store.Token.Scope, " "))
+ reqScope := oc.params.scope.Dup()
+ reqScope.Diff(tokenScope)
+ if reqScope.Len() > 0 {
+ return fmt.Errorf("cannot use stored token: missing scopes: \"%s\"",
+ strings.Join(reqScope.AsSlice(), "\", \""))
+ }
+
+ oc.store = &store
+ return nil
+}
+
+func (oc *oauthClient) storeCredentials(resp *oauthClientCredentials_AccessTokenReply) error {
+ now := time.Now()
+ store := oauthClientCredentials_Store{
+ TokenEndpoint: oc.params.tokenEndpoint,
+ ExpiresAt: now.Unix() + resp.ExpiresIn,
+ Token: *resp,
+ }
+
+ storeBytes, err := json.MarshalIndent(store, "", " ")
+ if err != nil {
+ return err
+ }
+
+ path := oc.credentialCachePath()
+ err = os.WriteFile(path, storeBytes, 0600)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
--- /dev/null
+package machines
+
+import (
+ "encoding/hex"
+ "net"
+ "time"
+)
+
+type Timestamp uint64
+type IPString string
+type HexEncoded string
+
+type WithUUID struct {
+ ID string `json:"id"`
+}
+
+type WithCalculatedName struct {
+ CalculatedName string `json:"__name__"`
+}
+
+type Host struct {
+ *WithUUID
+ *WithCalculatedName
+
+ Name string `json:"name"`
+ Owner *User `json:"owner,omitempty"`
+ Role string `json:"role"`
+ OS string `json:"os"`
+ CreatedAt Timestamp `json:"created_at"`
+ LastSeen Timestamp `json:"last_seen"`
+ LastSeenIface *Iface `json:"last_seen_iface"`
+ LastSeenIfaceID string `json:"last_seen_iface"`
+ Interfaces []*Iface `json:"interfaces"`
+}
+
+type User struct {
+ *WithUUID
+ *WithCalculatedName
+
+ Authorizer string `json:"authorizer"`
+ Principal string `json:"principal"`
+ CreatedAt Timestamp `json:"created_at"`
+ LastSeen Timestamp `json:"last_seen"`
+ Abilities []string `json:"abilities"`
+ Flags []string `json:"flags"`
+}
+
+type Iface struct {
+ *WithUUID
+ *WithCalculatedName
+
+ Host *Host `json:"host"`
+ HostID string `json:"host"`
+ Name string `json:"name"`
+ MediaType string `json:"type"`
+ HardwareAddress string `json:"hardware_address"`
+ LastIPv4 IPString `json:"last_inet4"`
+ LastIPv6 IPString `json:"last_inet6"`
+ LastSeen Timestamp `json:"last_seen"`
+ NameScrubbed string `json:"name_scrubbed"`
+ Reservations []*Reservation `json:"reservations"`
+}
+
+type Reservation struct {
+ *WithUUID
+ *WithCalculatedName
+
+ Iface *Iface `json:"iface"`
+ IfaceID string `json:"iface"`
+ AddressFamily string `json:"af"`
+ Address IPString `json:"address"`
+ Domain *Domain `json:"domain"`
+ Range *Range `json:"range"`
+}
+
+type Domain struct {
+ *WithUUID
+ *WithCalculatedName
+
+ Name string `json:"name"`
+ Site *Site `json:"site"`
+ SiteID string `json:"site"`
+ VlanID uint `json:"vlan_id"`
+
+ IPv4Address IPString `json:"inet4_address"`
+ IPv4PrefixLength uint8 `json:"inet4_prefixlen"`
+ IPv4RouterAddress IPString `json:"inet4_routeraddr"`
+
+ IPv6Address IPString `json:"inet6_address"`
+ IPv6PrefixLength uint8 `json:"inet6_prefixlen"`
+ IPv6RouterAddress IPString `json:"inet6_routeraddr"`
+
+ PXEServerIPv4 IPString `json:"pxe4_server"`
+ PXEServerIPv6 IPString `json:"pxe6_server"`
+ PXEFilenameBIOS string `json:"pxe_filename_bios"`
+ PXEFilenameUEFI string `json:"pxe_filename_uefi"`
+
+ Features []string `json:"features"`
+ DefaultRange *Range
+ ReverseDNSZoneIPv4 string `json:"inet4_reverse_zone"`
+ ReverseDNSZoneIPv6 string `json:"inet6_reverse_zone"`
+ GuestSeedStr HexEncoded `json:"guest_seed"`
+ GuestPassword string `json:"guest_password"`
+}
+
+type Range struct {
+ *WithUUID
+ *WithCalculatedName
+}
+
+type Site struct {
+ *WithUUID
+
+ Name string `json:"name"`
+}
+
+type EndorsementKey struct {
+ Found bool `json:"found"`
+ EndorsementKey struct {
+ PEM string `json:"pem"`
+ Type string `json:"type"`
+ Fingerprint struct {
+ SHA1 HexEncoded `json:"sha1"`
+ SHA256 HexEncoded `json:"sha256"`
+ } `json:"fingerprint"`
+ } `json:"endorsement_key"`
+}
+
+func (t Timestamp) AsTime() time.Time {
+ return time.Unix(int64(t), 0)
+}
+
+func (ip IPString) AsIP() net.IP {
+ return net.ParseIP(string(ip))
+}
+
+func (h HexEncoded) AsBytes() []byte {
+ ba, err := hex.DecodeString(string(h))
+ if err != nil {
+ return nil
+ }
+ return ba
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/mdlayher/apcupsd"
+ "go.fuhry.dev/runtime/metrics/metricbus/mbclient"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type apcMetrics struct {
+ lineVoltage, loadPercentage, secondsLeft, chargePct, powerConsumption, state mbclient.GaugeMetric
+ timeOnBattery mbclient.CounterMetric
+ stateMap map[string]mbclient.GaugeMetric
+}
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ flag.Parse()
+
+ svc := mbclient.NewService(ctx)
+
+ apcupsd, err := apcupsd.Dial("tcp", "127.0.0.1:3551")
+ if err != nil {
+ log.Panic(err)
+ }
+ defer apcupsd.Close()
+
+ metrics := apcMetrics{
+ lineVoltage: svc.DefineGauge("apcups_line_voltage", "AC mains line voltage"),
+ loadPercentage: svc.DefineGauge("apcups_load_pct", "Percentage of total load capacity used on the UPS"),
+ secondsLeft: svc.DefineGauge("apcups_battery_seconds_remaining", "UPS seconds of battery power remaining"),
+ chargePct: svc.DefineGauge("apcups_battery_charge_percentage", "UPS battery charge percentage"),
+ powerConsumption: svc.DefineGauge("apcups_power_consumption", "UPS estimate of current watt draw"),
+ state: svc.DefineGauge("apcups_power_state", "UPS power source", "state"),
+ timeOnBattery: svc.DefineCounter("apcups_battery_used_time", "UPS total time spent on battery"),
+ stateMap: make(map[string]mbclient.GaugeMetric, 0),
+ }
+
+ ticker := time.NewTicker(5 * time.Second)
+
+ log.Default().Infof("starting apcups_exporter")
+ defer svc.FlushAndWait()
+ for {
+ select {
+ case <-ticker.C:
+ status, err := apcupsd.Status()
+ if err != nil {
+ log.Default().Errorf("error getting status from apcupsd: %v", err)
+ continue
+ }
+ log.Default().V(1).Infof("UPS status: state=%s, line voltage: %.1f, load %% %.1f, seconds left %.0f, charge pct %.1f",
+ status.Status, status.LineVoltage, status.LoadPercent, status.TimeLeft.Seconds(), status.BatteryChargePercent)
+ metrics.lineVoltage.Set(status.LineVoltage)
+ metrics.loadPercentage.Set(status.LoadPercent)
+ metrics.secondsLeft.Set(status.TimeLeft.Seconds())
+ metrics.chargePct.Set(status.BatteryChargePercent)
+ metrics.powerConsumption.Set(float64(status.NominalPower) * (status.LoadPercent / 100))
+ if _, ok := metrics.stateMap[status.Status]; !ok {
+ metrics.stateMap[status.Status] = metrics.state.WithLabelValues(mbclient.KV{"state": status.Status})
+ }
+ for state, gauge := range metrics.stateMap {
+ if state == status.Status {
+ gauge.Set(1)
+ } else {
+ gauge.Set(0)
+ }
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
--- /dev/null
+# metrics bus
+
+**Purpose:** to allow individual runtime processes/services (and potentially others) to send
+metrics up to Prometheus without each one having to declare an individual metrics listener and
+scraper.
+
+**Methodology:** run a single metric collector and listener per host. Individual services use a
+push-based model to publish their metrics.
+
+## Transport
+
+Uses godbus on the system bus. The service identity is `dev.fuhry.runtime.metrics.MetricCollector`.
+
+## Methods
+
+### `Hello(string serviceName, string instanceDiscriminator) (string cookie)`
+
+Notifies the collector of the existence of a service (or an instance of a service).
+
+By convention, client libraries are expected to set `serviceName` to the basename of the executable
+running the service.
+
+The "instance discriminator" allows for instantiated services (that is, services for which multiple
+instances are run or may be run) to publish metrics independently.
+
+If a service with the given serviceName + instanceDiscriminator tuple is already registered by the
+collector, the collector will assume that the service instance exited abnormally and note this in
+the `metricbus_collector_ungraceful_restarts` counter.
+
+Returns a string containing a cookie which will be used by the client library for all future
+events for the process. Conventionally, this cookie will be a numeric connection ID which atomically
+increments for each connection, but clients must treat it as an opaque string value.
+
+### `Goodbye(string cookie)`
+
+Notifies the collector that a service is gracefully shutting down. Client implementations should
+automatically call this when they shut down (i.e. via canceling a context) to avoid incrementing the
+ungraceful_restarts or ungraceful_exits counters.
+
+### `Declare(string serviceCookie, uint16 metricType, string metricName, []string labels) (uint metricIndex)`
+
+Declares a new metric. metricType is one of:
+
+* `CounterMetric`
+* `GaugeMetric`
+
+All metrics have a `_task` label containing the service name.
+
+If the `serviceCookie` represents an instantiated service, the metric will also have an `_instance`
+label populated with the instance name.
+
+### `Post(string serviceCookie, []struct{uint64, float64, []string} values)`
+
+Post metric values. Clients **must** call `Post()` at an interval of 5 seconds or less; services not seen in >10 seconds are purged from memory.
+
+In the case of a gauge, the value of the gauge will be set to the specified value.
+
+In the case of a counter, the counter will be incremented by the specified value.
--- /dev/null
+package metricbus
+
+import (
+ "fmt"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type MetricType uint
+
+type CollectorError uint
+
+const (
+ CounterMetric MetricType = iota
+ GaugeMetric
+)
+
+const (
+ ErrUnspecified CollectorError = iota
+ ErrServiceCookieNotFound
+ ErrMetricNotFound
+)
+
+const DbusServiceName = "dev.fuhry.runtime.metrics.MetricCollector.v1"
+const DbusServicePath = "/dev/fuhry/runtime/metrics/MetricCollector"
+const SingletonInstanceDiscriminator = "GLOBAL"
+
+type KV map[string]string
+
+func (kv KV) Equals(other KV) bool {
+ if len(kv) != len(other) {
+ return false
+ }
+
+ for k, v := range kv {
+ ov, ok := other[k]
+ if !ok || ov != v {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (mt MetricType) String() string {
+ switch mt {
+ case CounterMetric:
+ return "counter"
+ case GaugeMetric:
+ return "gauge"
+ }
+
+ panic(fmt.Errorf("unknown metrictype value: %d", uint(mt)))
+}
+
+func (err CollectorError) Error() string {
+ switch err {
+ case ErrUnspecified:
+ return DbusServiceName + ".Error.Unspecified"
+ case ErrServiceCookieNotFound:
+ return DbusServiceName + ".Error.ServiceCookieNotFound"
+ case ErrMetricNotFound:
+ return DbusServiceName + ".Error.MetricNotFound"
+ default:
+ return DbusServiceName + fmt.Sprintf(".Error.Unknown.%d", uint(err))
+ }
+}
+
+// DbusError implements
+func (err CollectorError) DbusError() (string, []interface{}) {
+ return err.Error(), []interface{}{}
+}
+
+func (err CollectorError) DbusErrorS() *dbus.Error {
+ name, body := err.DbusError()
+ return &dbus.Error{
+ Name: name,
+ Body: body,
+ }
+}
+
+func (err CollectorError) Equals(other error) bool {
+ if other == nil {
+ return false
+ }
+ if dbe, ok := other.(dbus.Error); ok {
+ return dbe.Name == err.Error()
+ }
+
+ return false
+}
--- /dev/null
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<busconfig>
+ <policy user="node_exporter">
+ <allow own="dev.fuhry.runtime.metrics.MetricCollector.v1" />
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1" />
+ <allow receive_sender="dev.fuhry.runtime.metrics.MetricCollector.v1" />
+ </policy>
+
+ <policy context="default">
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_interface="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_member="Ping" />
+
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_interface="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_member="Hello" />
+
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_interface="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_member="Goodbye" />
+
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_interface="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_member="Declare" />
+
+ <allow send_destination="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_interface="dev.fuhry.runtime.metrics.MetricCollector.v1"
+ send_member="Post" />
+ </policy>
+</busconfig>
--- /dev/null
+package internal
+
+import (
+ "flag"
+
+ "github.com/godbus/dbus/v5"
+ "github.com/godbus/dbus/v5/introspect"
+ "go.fuhry.dev/runtime/metrics/metricbus"
+)
+
+var useSessionBus bool
+
+func init() {
+ flag.BoolVar(&useSessionBus, "metricbus.session-bus", false, "use the D-Bus session bus instead of the system bus")
+}
+
+func DbusConn() (*dbus.Conn, error) {
+ if useSessionBus {
+ return dbus.ConnectSessionBus()
+ }
+ return dbus.ConnectSystemBus()
+}
+
+const dbusInterface = `
+<node>
+ <interface name="` + metricbus.DbusServiceName + `">
+ <method name="Ping">
+ </method>
+ <method name="Hello">
+ <arg name="service_name" direction="in" type="s" />
+ <arg name="instance_discriminator" direction="in" type="s" />
+ <arg name="cookie" direction="out" type="s" />
+ </method>
+ <method name="Goodbye">
+ <arg name="cookie" direction="in" type="s" />
+ </method>
+ <method name="Declare">
+ <arg name="cookie" direction="in" type="s" />
+ <arg name="metric_type" direction="in" type="u" />
+ <arg name="metric_name" direction="in" type="s" />
+ <arg name="metric_help" direction="in" type="s" />
+ <arg name="labels" direction="in" type="a{s}" />
+ <arg name="metric_id" direction="out" type="u" />
+ </method>
+ <method name="Post">
+ <arg name="cookie" direction="in" type="s" />
+ <arg name="values" direction="in" type="a{(dda{s})}" />
+ </method>
+ </interface>
+ ` + introspect.IntrospectDataString + `
+</node>
+`
--- /dev/null
+package internal
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+ "github.com/godbus/dbus/v5/introspect"
+ "go.fuhry.dev/runtime/metrics/metricbus"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+ "go.uber.org/atomic"
+)
+
+type metaMetric struct {
+ metricId uint64
+ servicer *mbServicer
+}
+
+type mbMetaMetrics struct {
+ ungracefulShutdowns *metaMetric
+ ungracefulRestarts *metaMetric
+}
+
+type mbMetricValue struct {
+ value *atomic.Float64
+ labels []string
+}
+
+type mbMetricValueArg struct {
+ MetricId uint64
+ Value float64
+ LabelValues []string
+}
+
+type mbMetric struct {
+ metricType metricbus.MetricType
+ name string
+ help string
+ valueMu sync.Mutex
+ validLabels []string
+ values []*mbMetricValue
+}
+
+type mbService struct {
+ name string
+ instance string
+ lastPost time.Time
+ deregisterPending bool
+
+ metrics map[uint64]*mbMetric
+ nextId *atomic.Uint64
+}
+
+type mbServicer struct {
+ log *log.Logger
+ serviceLock sync.RWMutex
+ services map[uint64]*mbService
+ serviceSet *hashset.HashSet[string]
+ nextId *atomic.Uint64
+
+ metaCookie string
+ metaMetrics mbMetaMetrics
+}
+
+type mbServer struct {
+ log *log.Logger
+ ctx context.Context
+ servicer *mbServicer
+ httpServer *http.Server
+ httpMux *http.ServeMux
+ listenOn *net.TCPAddr
+ startStopMu sync.Mutex
+}
+
+var defaultMetricBusHttpServerPort uint
+
+func init() {
+ flag.UintVar(&defaultMetricBusHttpServerPort, "metricbus.server.port", 7082, "https server port to listen on")
+}
+
+func NewMetricBusServer() (*mbServer, error) {
+ if !flag.Parsed() {
+ return nil, errors.New("flags have not been parsed, cannot determine server port")
+ }
+
+ return NewMetricBusServerWithPort(defaultMetricBusHttpServerPort)
+}
+
+func makeTlsListener(tcpAddr *net.TCPAddr, ctx context.Context) (net.Listener, error) {
+ mtlsId := mtls.DefaultIdentity()
+
+ netListener, err := net.ListenTCP("tcp", tcpAddr)
+ if err != nil {
+ return nil, err
+ }
+ tlsc, err := mtlsId.TlsConfig(ctx)
+ if err != nil {
+ return nil, err
+ }
+ cv := mtls.NewPeerNameVerifier()
+ err = cv.ConfigureServer(tlsc)
+ if err != nil {
+ return nil, err
+ }
+ cv.AllowFrom(mtls.Service, "prometheus")
+ cv.AllowFrom(mtls.User, "dan")
+
+ return tls.NewListener(netListener, tlsc), nil
+}
+
+func NewMetricBusServerWithPort(port uint) (*mbServer, error) {
+ tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ return nil, err
+ }
+
+ logger := log.WithPrefix("metricbus.server")
+
+ servicer, err := newMetricBusServicer(logger)
+ if err != nil {
+ return nil, err
+ }
+
+ handler := http.NewServeMux()
+ handler.HandleFunc("/metrics", servicer.metricsToString)
+ server := &mbServer{
+ log: logger,
+ httpMux: handler,
+ httpServer: &http.Server{
+ Handler: handler,
+ },
+ listenOn: tcpAddr,
+ servicer: servicer,
+ }
+
+ server.log.V(1).Infof("Server configured to listen on %s", tcpAddr.String())
+
+ return server, nil
+}
+
+func (s *mbServer) Start(ctx context.Context) error {
+ s.startStopMu.Lock()
+ defer s.startStopMu.Unlock()
+
+ s.ctx = ctx
+ // startup dbus server
+ dbusConn, err := DbusConn()
+ if err != nil {
+ return err
+ }
+
+ s.log.V(2).Notice("Connected to D-Bus")
+
+ dbusConn.Export(s.servicer, metricbus.DbusServicePath, metricbus.DbusServiceName)
+ dbusConn.Export(introspect.Introspectable(dbusInterface), metricbus.DbusServicePath, "org.fredesktop.DBus.Introspectable")
+
+ reply, err := dbusConn.RequestName(metricbus.DbusServiceName, dbus.NameFlagDoNotQueue)
+ if err != nil {
+ return err
+ }
+ if reply != dbus.RequestNameReplyPrimaryOwner {
+ return errors.New("dbus name already taken")
+ }
+
+ s.log.V(2).Noticef("Successfully registered D-Bus interface: %s", metricbus.DbusServiceName)
+
+ // startup http server
+ s.log.V(2).Notice("Starting TLS server")
+ listener, err := makeTlsListener(s.listenOn, ctx)
+ if err != nil {
+ return err
+ }
+
+ go s.httpServer.Serve(listener)
+ go (func() {
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+
+ defer s.startStopMu.Unlock()
+ defer cancel()
+ defer dbusConn.Close()
+ defer s.httpServer.Shutdown(shutdownCtx)
+
+ ticker := time.NewTicker(1 * time.Second)
+ for {
+ select {
+ case <-ticker.C:
+ s.servicer.sweepDeadServices()
+ case <-s.ctx.Done():
+ s.startStopMu.Lock()
+ return
+ }
+ }
+ })()
+
+ s.log.Notice("Server started")
+
+ return nil
+}
+
+func newMetricBusServicer(log *log.Logger) (*mbServicer, error) {
+ s := &mbServicer{
+ log: log.AppendPrefix(".servicer"),
+ services: make(map[uint64]*mbService, 0),
+ serviceSet: hashset.NewHashSet[string](),
+ nextId: atomic.NewUint64(0),
+ }
+
+ metaCookie, err := s.Hello("metricbus_collector", "")
+ if err != nil {
+ return nil, err
+ }
+ s.metaCookie = metaCookie
+ ungracefulShutdownsMetricId, err := s.Declare(s.metaCookie, metricbus.CounterMetric, "metricbus_collector_ungraceful_shutdowns", "Tracks the number of un-graceful shutdowns (services which have not posted metrics in >10s)", []string{"service", "instance"})
+ if err != nil {
+ return nil, err
+ }
+ ungracefulRestartsMetricId, err := s.Declare(s.metaCookie, metricbus.CounterMetric, "metricbus_collector_ungraceful_restarts", "Tracks the number of un-graceful restarts (services which declared a name/discriminator pair that was already taken)", []string{"service", "instance"})
+ if err != nil {
+ return nil, err
+ }
+
+ s.metaMetrics = mbMetaMetrics{
+ ungracefulShutdowns: &metaMetric{
+ metricId: ungracefulShutdownsMetricId,
+ servicer: s,
+ },
+ ungracefulRestarts: &metaMetric{
+ metricId: ungracefulRestartsMetricId,
+ servicer: s,
+ },
+ }
+
+ return s, nil
+}
+
+func (s *mbServicer) metricsToString(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("content-type", "text/plain; version=0.0.4; charset=utf-8")
+
+ for cookie, service := range s.services {
+ for _, metric := range service.metrics {
+ metricName := metric.name // fmt.Sprintf("%s_%s", service.name, metric.name)
+ fmt.Fprintf(
+ w,
+ "# HELP %s %s\n",
+ metricName,
+ metric.help,
+ )
+ fmt.Fprintf(
+ w,
+ "# TYPE %s %s\n",
+ metricName,
+ metric.metricType.String(),
+ )
+
+ baseLabelValues := []string{
+ fmt.Sprintf("%s=%q", "_task", service.name),
+ fmt.Sprintf("%s=%q", "_host", hostname.Fqdn()),
+ }
+ if service.instance != metricbus.SingletonInstanceDiscriminator {
+ baseLabelValues = append(baseLabelValues, fmt.Sprintf("%s=%s", "_instance", service.instance))
+ }
+
+ for _, value := range metric.values {
+ labels := make([]string, len(baseLabelValues))
+ copy(labels, baseLabelValues)
+
+ for i, labelName := range metric.validLabels {
+ if value.labels[i] != "" {
+ labels = append(labels, fmt.Sprintf("%s=%q", labelName, value.labels[i]))
+ }
+ }
+
+ fmt.Fprintf(
+ w,
+ "%s{%s} %.2f\n",
+ metricName,
+ strings.Join(labels, ","),
+ value.value.Load(),
+ )
+ }
+
+ fmt.Fprintf(w, "\n")
+ }
+
+ if service.deregisterPending {
+ s.log.Infof("deregistering dead service %s/%s with cookie value %d", service.name, service.instance, cookie)
+ delete(s.services, cookie)
+ }
+ }
+}
+
+func (s *mbServicer) Ping() *dbus.Error {
+ return nil
+}
+
+func (s *mbServicer) Hello(serviceName string, instanceDiscriminator string) (string, *dbus.Error) {
+ s.serviceLock.Lock()
+ defer s.serviceLock.Unlock()
+
+ if instanceDiscriminator == "" {
+ instanceDiscriminator = metricbus.SingletonInstanceDiscriminator
+ }
+ serviceKey := fmt.Sprintf("%s/%s", serviceName, instanceDiscriminator)
+ if s.serviceSet.Contains(serviceKey) {
+ s.log.Warnf("detected an ungraceful restart for service %s", serviceKey)
+ s.metaMetrics.ungracefulRestarts.Set(1.0, metricbus.KV{
+ "service": serviceName,
+ "instance": instanceDiscriminator,
+ })
+ }
+
+ id := s.nextId.Inc()
+
+ s.services[id] = &mbService{
+ name: serviceName,
+ instance: instanceDiscriminator,
+ lastPost: time.Now(),
+ metrics: make(map[uint64]*mbMetric, 0),
+ nextId: atomic.NewUint64(0),
+ }
+ s.serviceSet.Add(serviceKey)
+
+ s.log.Noticef("service %s registered", serviceKey)
+
+ return fmt.Sprintf("%d", id), nil
+}
+
+func (s *mbServicer) Goodbye(cookie string) *dbus.Error {
+ s.serviceLock.Lock()
+ defer s.serviceLock.Unlock()
+
+ serviceIndex, err := strconv.ParseUint(cookie, 10, 64)
+ if err != nil {
+ return metricbus.ErrServiceCookieNotFound.DbusErrorS()
+ }
+
+ if _, ok := s.services[serviceIndex]; !ok {
+ return &dbus.ErrMsgNoObject
+ }
+
+ serviceKey := fmt.Sprintf("%s/%s", s.services[serviceIndex].name, s.services[serviceIndex].instance)
+
+ s.services[serviceIndex].deregisterPending = true
+ s.serviceSet.Del(serviceKey)
+
+ s.log.Noticef("service %s deregistered gracefully", serviceKey)
+
+ return nil
+}
+
+func (s *mbServicer) Declare(serviceCookie string, metricType metricbus.MetricType, metricName string, metricHelp string, labels []string) (uint64, *dbus.Error) {
+ s.serviceLock.RLock()
+ defer s.serviceLock.RUnlock()
+
+ service, err := s.serviceFromCookie(serviceCookie)
+ if err != nil {
+ return 0, metricbus.ErrServiceCookieNotFound.DbusErrorS()
+ }
+ metricId := service.nextId.Inc()
+
+ service.metrics[metricId] = &mbMetric{
+ metricType: metricType,
+ name: metricName,
+ help: metricHelp,
+ validLabels: labels,
+ values: make([]*mbMetricValue, 0),
+ }
+
+ s.log.Noticef("service %s/%s declared %s metric with id %d: %s", service.name, service.instance, metricType.String(), metricId, metricName)
+
+ return metricId, nil
+}
+
+func (s *mbServicer) Post(serviceCookie string, values []mbMetricValueArg) *dbus.Error {
+ s.serviceLock.RLock()
+ defer s.serviceLock.RUnlock()
+
+ service, err := s.serviceFromCookie(serviceCookie)
+ if err != nil {
+ return metricbus.ErrServiceCookieNotFound.DbusErrorS()
+ }
+
+ s.log.V(2).Debugf("service %q posted update: %+v", service.name, values)
+
+ service.lastPost = time.Now()
+
+ for _, value := range values {
+ metric, ok := service.metrics[value.MetricId]
+ if !ok {
+ return metricbus.ErrMetricNotFound.DbusErrorS()
+ }
+
+ err = metric.post(value.Value, value.LabelValues)
+ if err != nil {
+ return dbus.MakeFailedError(err)
+ }
+ }
+
+ return nil
+}
+
+func (s *mbServicer) serviceFromCookie(serviceCookie string) (*mbService, error) {
+ serviceIndex, err := strconv.ParseUint(serviceCookie, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse service cookie as uint64: %s", serviceCookie)
+ }
+
+ if service, ok := s.services[serviceIndex]; ok {
+ return service, nil
+ }
+
+ return nil, fmt.Errorf("failed to lookup service with cookie %s", serviceCookie)
+}
+
+func (s *mbServicer) sweepDeadServices() {
+ s.serviceLock.Lock()
+ defer s.serviceLock.Unlock()
+
+ for c, service := range s.services {
+ if fmt.Sprintf("%d", c) == s.metaCookie {
+ continue
+ }
+ if service.deregisterPending {
+ continue
+ }
+
+ if service.lastPost.Add(10 * time.Second).Before(time.Now()) {
+ s.log.Warnf("service %s/%s (cookie %d) not seen since %s, recording unsuccessful shutdown",
+ service.name,
+ service.instance,
+ c,
+ service.lastPost)
+
+ s.metaMetrics.ungracefulShutdowns.Set(1.0, metricbus.KV{
+ "service": service.name,
+ "instance": service.instance,
+ })
+
+ serviceKey := fmt.Sprintf("%s/%s", service.name, service.instance)
+ s.serviceSet.Del(serviceKey)
+ s.services[c].deregisterPending = true
+ }
+ }
+}
+
+func (m *mbMetric) post(val float64, labelValues []string) error {
+ m.valueMu.Lock()
+ defer m.valueMu.Unlock()
+
+ if len(labelValues) != len(m.validLabels) {
+ return fmt.Errorf("value for metric %s does not contain the correct number of label values (%d)", m.name, len(m.validLabels))
+ }
+
+ for _, mv := range m.values {
+ if mv.LabelsEqual(labelValues) {
+ switch m.metricType {
+ case metricbus.CounterMetric:
+ mv.value.Add(val)
+ case metricbus.GaugeMetric:
+ mv.value.Store(val)
+ default:
+ log.Default().WithPrefix("metricbus.server.mbMetric").Panicf("metric %s: unknown metric type: %d", m.name, m.metricType)
+ }
+
+ return nil
+ }
+ }
+
+ valueEntry := &mbMetricValue{
+ value: atomic.NewFloat64(val),
+ labels: labelValues,
+ }
+ m.values = append(m.values, valueEntry)
+
+ return nil
+}
+
+func (v *mbMetricValue) LabelsEqual(values []string) bool {
+ // NOTE: will cause a panic if lengths are not pre-checked!
+ for i := range v.labels {
+ if v.labels[i] != values[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (m *metaMetric) Set(val float64, labels metricbus.KV) {
+ service, err := m.servicer.serviceFromCookie(m.servicer.metaCookie)
+ if err != nil {
+ return
+ }
+ metric := service.metrics[m.metricId]
+ labelValues := make([]string, len(metric.validLabels))
+ for i, label := range metric.validLabels {
+ if v, ok := labels[label]; ok {
+ labelValues[i] = v
+ }
+ }
+
+ metric.post(val, labelValues)
+}
--- /dev/null
+package mbclient
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "path"
+ "strings"
+ "sync"
+ "time"
+
+ "go.fuhry.dev/runtime/metrics/metricbus"
+ "go.fuhry.dev/runtime/utils/log"
+ "go.uber.org/atomic"
+)
+
+type KV = metricbus.KV
+
+type MetricBusService struct {
+ name string
+ instance string
+
+ log *log.Logger
+
+ client metricBusLowLevelConnection
+ ctx context.Context
+ childCtx context.Context
+ childCtxCancel context.CancelFunc
+
+ eventQ []metricValue
+ metrics map[string]*metricMetadata
+
+ mu sync.Mutex
+
+ serviceCookie string
+}
+
+var defaultMetricBusServiceName string
+var defaultMetricBusServiceDiscriminator string
+
+func init() {
+ taskName := strings.ReplaceAll(path.Base(os.Args[0]), "-", "_")
+ flag.StringVar(&defaultMetricBusServiceName, "metricbus.client.service-name", taskName, "service name to use for publishing metrics")
+ flag.StringVar(&defaultMetricBusServiceDiscriminator, "metricbus.client.service-discriminator", "", "discriminator to use by default for publishing metrics")
+}
+
+func NewService(ctx context.Context) *MetricBusService {
+ return NewServiceWithDiscriminator(ctx, defaultMetricBusServiceDiscriminator)
+}
+
+func NewServiceWithDiscriminator(ctx context.Context, instanceDiscriminator string) *MetricBusService {
+ childCtx, cancel := context.WithCancel(context.Background())
+ svc := &MetricBusService{
+ name: defaultMetricBusServiceName,
+ instance: instanceDiscriminator,
+
+ log: log.WithPrefix(fmt.Sprintf("metricbus.client[%s]", defaultMetricBusServiceName)),
+ eventQ: make([]metricValue, 0),
+ metrics: make(map[string]*metricMetadata, 0),
+
+ client: newLowLevelClient(childCtx),
+ ctx: ctx,
+ childCtx: childCtx,
+ childCtxCancel: cancel,
+ }
+
+ go svc.loop()
+
+ svc.tryRegister()
+
+ return svc
+}
+func (s *MetricBusService) FlushAndWait() {
+ if s.childCtx.Err() == context.Canceled || s.serviceCookie == "" {
+ return
+ }
+
+ s.Flush()
+ <-s.childCtx.Done()
+
+ // wait on the mutex to be released
+ s.mu.Lock()
+ defer s.mu.Unlock()
+}
+
+func (s *MetricBusService) DefineCounter(metricName, descr string, labelNames ...string) CounterMetric {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ bm := &baseMetric{
+ metricMetadata: &metricMetadata{
+ s: s,
+ name: metricName,
+ help: descr,
+ metricType: metricbus.CounterMetric,
+ labelNames: labelNames,
+ },
+ labelValues: make([]string, len(labelNames)),
+ }
+
+ if err := bm.EnsureDeclared(); err != nil {
+ s.log.Warn("failed to declare metric, will retry when publishing: ", err)
+ }
+
+ s.metrics[metricName] = bm.metricMetadata
+
+ return &counterMetric{
+ baseMetric: bm,
+ }
+}
+
+func (s *MetricBusService) DefineGauge(metricName, descr string, labelNames ...string) GaugeMetric {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ bm := &baseMetric{
+ metricMetadata: &metricMetadata{
+ s: s,
+ name: metricName,
+ help: descr,
+ metricType: metricbus.GaugeMetric,
+ labelNames: labelNames,
+ },
+ labelValues: make([]string, len(labelNames)),
+ }
+
+ if err := bm.EnsureDeclared(); err != nil {
+ s.log.Warn("failed to declare metric, will retry when publishing: ", err)
+ }
+
+ s.metrics[metricName] = bm.metricMetadata
+
+ return &gaugeMetric{
+ baseMetric: bm,
+ }
+}
+
+func (s *MetricBusService) setLocalStateDeregistered() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ s.serviceCookie = ""
+
+ // clear metric IDs so that these are recreated
+ for _, metric := range s.metrics {
+ metric.metricId = 0
+ }
+}
+
+func (s *MetricBusService) tryRegister() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.serviceCookie != "" {
+ return
+ }
+
+ cookie, err := s.client.Hello(s.name, s.instance)
+ if err != nil {
+ s.log.Warn("failed to register service with MetricBus server: ", err)
+ }
+
+ s.serviceCookie = cookie
+}
+
+func (s *MetricBusService) Flush() {
+ var err error
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+flushRestart:
+ for _, m := range s.metrics {
+ err = m.EnsureDeclared()
+ if err != nil {
+ if metricbus.ErrServiceCookieNotFound.Equals(err) {
+ goto handleServerRestarted
+ }
+
+ s.log.Warn("failed to declare metric: ", err)
+ }
+ }
+
+ for i := range s.eventQ {
+ ev := &s.eventQ[i]
+
+ baseMetric := s.metrics[ev.metricName]
+ ev.MetricId = baseMetric.metricId
+ ev.ExportValue = ev.value.Load()
+ }
+
+ err = s.client.Post(s.serviceCookie, s.eventQ)
+ if err != nil {
+ if metricbus.ErrServiceCookieNotFound.Equals(err) {
+ goto handleServerRestarted
+ }
+ s.log.Warn("failed to post events, queue will remain unflushed: ", err)
+ return
+ }
+
+ s.eventQ = make([]metricValue, 0)
+ return
+
+handleServerRestarted:
+ s.log.Notice("Server was restarted! Resetting local state and re-registering with collector.")
+
+ s.mu.Unlock()
+ s.setLocalStateDeregistered()
+ s.tryRegister()
+ s.mu.Lock()
+
+ goto flushRestart
+}
+
+func (s *MetricBusService) deregister() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.serviceCookie == "" {
+ return
+ }
+
+ err := s.client.Goodbye(s.serviceCookie)
+ if err != nil {
+ s.log.Warn("failed to deregister service from metric collector: ", err)
+ }
+
+ s.childCtxCancel()
+}
+
+func (s *MetricBusService) loop() {
+ ticker := time.NewTicker(1 * time.Second)
+
+ for {
+ select {
+ case <-s.ctx.Done():
+ s.deregister()
+ return
+ case <-ticker.C:
+ s.Flush()
+ }
+ }
+}
+
+func (s *MetricBusService) event(metricName string, labelValues []string) *metricValue {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if metricName == "" {
+ s.log.Panic("empty metric names are not permitted")
+ }
+
+ mv := metricValue{
+ metricName: metricName,
+ LabelValues: labelValues,
+ }
+
+ for _, qmv := range s.eventQ {
+ if qmv.Equals(mv) {
+ return &qmv
+ }
+ }
+
+ mv.value = atomic.NewFloat64(0.0)
+
+ s.eventQ = append(s.eventQ, mv)
+ return &s.eventQ[len(s.eventQ)-1]
+}
--- /dev/null
+package mbclient
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+ "go.fuhry.dev/runtime/metrics/metricbus"
+ mbinternal "go.fuhry.dev/runtime/metrics/metricbus/internal"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+var globalDbusConn *dbus.Conn
+var globalDbusConnOnce sync.Once
+
+var globalDbusMetricBusObj dbus.BusObject
+var globalDbusMetricBusObjMu sync.Mutex
+
+const metricBusApiPrefix = metricbus.DbusServiceName
+
+type metricBusLowLevelClient struct {
+ ctx context.Context
+}
+
+func newDbusConnection(ctx context.Context) (*dbus.Conn, error) {
+ conn, err := mbinternal.DbusConn()
+ if err != nil {
+ return nil, err
+ }
+
+ go (func() {
+ <-ctx.Done()
+ conn.Close()
+ })()
+
+ return conn, nil
+}
+
+func mustGlobalDbusConn(ctx context.Context) *dbus.Conn {
+ globalDbusConnOnce.Do(func() {
+ conn, err := newDbusConnection(ctx)
+ if err != nil {
+ log.Panic(err)
+ }
+ if conn == nil {
+ log.Panic("dbus session connection is nil, but so was err??")
+ }
+
+ globalDbusConn = conn
+ })
+
+ return globalDbusConn
+}
+
+func metricBusDbusObject(ctx context.Context) (dbus.BusObject, error) {
+ globalDbusMetricBusObjMu.Lock()
+ defer globalDbusMetricBusObjMu.Unlock()
+
+ if globalDbusMetricBusObj == nil {
+ conn := mustGlobalDbusConn(ctx)
+ globalDbusMetricBusObj = conn.Object(metricBusApiPrefix, metricbus.DbusServicePath)
+
+ if globalDbusMetricBusObj == nil {
+ log.Default().Errorf("failed to get BusObject")
+ return nil, fmt.Errorf("failed to get BusObject")
+ }
+ }
+
+ call := globalDbusMetricBusObj.Call(metricBusApiPrefix+".Ping", dbus.Flags(0))
+ if call.Err != nil {
+ log.Default().Error("failed to ping MetricBus server: ", call.Err)
+ globalDbusMetricBusObj = nil
+ return nil, call.Err
+ }
+
+ return globalDbusMetricBusObj, nil
+}
+
+func newLowLevelClient(ctx context.Context) metricBusLowLevelConnection {
+ return &metricBusLowLevelClient{
+ ctx: ctx,
+ }
+}
+
+func (c *metricBusLowLevelClient) Ping() error {
+ obj, err := metricBusDbusObject(c.ctx)
+ if err != nil {
+ return err
+ }
+
+ result := obj.Call(metricBusApiPrefix+".Ping", dbus.Flags(0))
+ if result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func (c *metricBusLowLevelClient) Hello(serviceName, instanceDiscriminator string) (serviceCookie string, err error) {
+ obj, err := metricBusDbusObject(c.ctx)
+ if err != nil {
+ return "", err
+ }
+
+ result := obj.Call(metricBusApiPrefix+".Hello", dbus.Flags(0), serviceName, instanceDiscriminator)
+ if result.Err != nil {
+ return "", result.Err
+ }
+
+ result.Store(&serviceCookie)
+ return serviceCookie, nil
+}
+
+func (c *metricBusLowLevelClient) Goodbye(serviceCookie string) error {
+ obj, err := metricBusDbusObject(c.ctx)
+ if err != nil {
+ return err
+ }
+
+ result := obj.Call(metricBusApiPrefix+".Goodbye", dbus.Flags(0), serviceCookie)
+ if result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func (c *metricBusLowLevelClient) Declare(serviceCookie string, metricType metricbus.MetricType, metricName string, metricHelp string, labels ...string) (metricId uint64, err error) {
+ obj, err := metricBusDbusObject(c.ctx)
+ if err != nil {
+ return 0, err
+ }
+
+ result := obj.Call(metricBusApiPrefix+".Declare",
+ dbus.Flags(0),
+ serviceCookie,
+ uint16(metricType),
+ metricName,
+ metricHelp,
+ labels)
+
+ if result.Err != nil {
+ return 0, result.Err
+ }
+
+ result.Store(&metricId)
+ return metricId, nil
+}
+
+func (c *metricBusLowLevelClient) Post(serviceCookie string, values []metricValue) error {
+ obj, err := metricBusDbusObject(c.ctx)
+ if err != nil {
+ return err
+ }
+
+ result := obj.Call(metricBusApiPrefix+".Post",
+ dbus.Flags(0),
+ serviceCookie,
+ values)
+
+ if result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "math/rand"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "go.fuhry.dev/runtime/metrics/metricbus/mbclient"
+)
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ flag.Parse()
+
+ svc := mbclient.NewServiceWithDiscriminator(ctx, "merr")
+
+ counter := svc.DefineCounter("test_count", "test counter", "labelone", "labeltwo")
+ gauge := svc.DefineGauge("test_gauge", "test gauge", "label_a", "label_b")
+
+ counterLabelSetOne := counter.WithLabelValues(mbclient.KV{"labelone": "foo", "labeltwo": "bar"})
+ counterLabelSetTwo := counter.WithLabelValues(mbclient.KV{"labelone": "baz", "labeltwo": "quux"})
+
+ gaugeLabelSetOne := gauge.WithLabelValues(mbclient.KV{"label_a": "one", "label_b": "two"})
+ gaugeLabelSetTwo := gauge.WithLabelValues(mbclient.KV{"label_a": "three", "label_b": "four"})
+
+ ticker := time.NewTicker(5 * time.Second)
+
+ defer svc.FlushAndWait()
+ for {
+ select {
+ case <-ticker.C:
+ counterLabelSetOne.Add(rand.Float64())
+ counterLabelSetTwo.Add(rand.Float64())
+ gaugeLabelSetOne.Set(rand.Float64())
+ gaugeLabelSetTwo.Set(rand.Float64())
+ case <-ctx.Done():
+ return
+ }
+ }
+}
--- /dev/null
+package mbclient
+
+import (
+ "go.fuhry.dev/runtime/metrics/metricbus"
+ "go.uber.org/atomic"
+)
+
+type metricValue struct {
+ MetricId uint64
+ ExportValue float64
+ LabelValues []string
+
+ metricName string
+ value *atomic.Float64
+}
+
+type metricBusLowLevelConnection interface {
+ Ping() error
+ Hello(serviceName, instance string) (string, error)
+ Goodbye(cookie string) error
+ Declare(serviceCookie string, metricType metricbus.MetricType, metricName string, metricHelp string, labels ...string) (uint64, error)
+ Post(serviceCookie string, values []metricValue) error
+}
+
+type CounterMetric interface {
+ WithLabelValues(metricbus.KV) CounterMetric
+ Add(float64)
+}
+
+type GaugeMetric interface {
+ WithLabelValues(metricbus.KV) GaugeMetric
+ Set(float64)
+ Reset()
+}
--- /dev/null
+package mbclient
+
+import (
+ "go.fuhry.dev/runtime/metrics/metricbus"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type metricMetadata struct {
+ s *MetricBusService
+ name string
+ help string
+ metricType metricbus.MetricType
+ labelNames []string
+ metricId uint64
+}
+
+type baseMetric struct {
+ *metricMetadata
+ labelValues []string
+}
+
+type counterMetric struct {
+ *baseMetric
+}
+
+type gaugeMetric struct {
+ *baseMetric
+}
+
+func (v metricValue) Equals(other metricValue) bool {
+ if v.metricName != other.metricName {
+ return false
+ }
+ for i := range v.LabelValues {
+ if v.LabelValues[i] != other.LabelValues[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (m *metricMetadata) EnsureDeclared() error {
+ if m.metricId != 0 {
+ return nil
+ }
+
+ metricId, err := m.s.client.Declare(
+ m.s.serviceCookie,
+ m.metricType,
+ m.name,
+ m.help,
+ m.labelNames...,
+ )
+ if err != nil {
+ return err
+ }
+
+ log.Default().WithPrefix("metricbus.mbclient").V(1).Debugf("declared metric %q with id %d",
+ m.name, metricId)
+
+ m.metricId = metricId
+
+ return nil
+}
+
+func (m *baseMetric) WithLabelValues(kv metricbus.KV) *baseMetric {
+ newMetric := &baseMetric{
+ metricMetadata: m.metricMetadata,
+ labelValues: make([]string, len(m.labelNames)),
+ }
+
+ n := len(kv)
+ for i, k := range m.labelNames {
+ if val, ok := kv[k]; ok {
+ newMetric.labelValues[i] = val
+ n--
+ }
+ }
+
+ if n > 0 {
+ log.Default().Panicf("metric got kv with unrecognized labels: %+v, valid labels: %+v",
+ kv, m.labelNames)
+ }
+
+ return newMetric
+}
+
+func (m *counterMetric) Add(val float64) {
+ qent := m.s.event(m.name, m.labelValues)
+ qent.value.Add(val)
+}
+
+func (m *counterMetric) WithLabelValues(kv metricbus.KV) CounterMetric {
+ newMetric := &counterMetric{
+ baseMetric: m.baseMetric.WithLabelValues(kv),
+ }
+
+ return newMetric
+}
+
+func (m *gaugeMetric) Set(val float64) {
+ qent := m.s.event(m.name, m.labelValues)
+ qent.value.Store(val)
+}
+
+func (m *gaugeMetric) Reset() {
+ m.Set(0.0)
+}
+
+func (m *gaugeMetric) WithLabelValues(kv metricbus.KV) GaugeMetric {
+ newMetric := &gaugeMetric{
+ baseMetric: m.baseMetric.WithLabelValues(kv),
+ }
+
+ return newMetric
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "syscall"
+
+ mbinternal "go.fuhry.dev/runtime/metrics/metricbus/internal"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ serverCtx, cancel := context.WithCancel(context.Background())
+
+ flag.Parse()
+
+ mbs, err := mbinternal.NewMetricBusServer()
+ if err != nil {
+ log.Panic(err)
+ }
+
+ err = mbs.Start(serverCtx)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ <-ctx.Done()
+ cancel()
+}
--- /dev/null
+[Unit]
+Description=Metric Collector
+
+[Service]
+Type=dbus
+User=node_exporter
+BusName=dev.fuhry.runtime.metrics.MetricCollector.v1
+ExecStart=/usr/bin/metricbus-collector -mtls.id=node-exporter
+
+[Install]
+Alias=dev.fuhry.runtime.metrics.MetricCollector.service
+WantedBy=default.target
--- /dev/null
+package certutil
+
+import (
+ "crypto"
+ "crypto/x509"
+ "encoding/asn1"
+ "encoding/pem"
+ "fmt"
+ "net/url"
+ "os"
+ "strings"
+)
+
+var oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
+
+func LoadCertificatesFromPEM(path string) ([]*x509.Certificate, error) {
+ var pemBlock *pem.Block
+
+ certs := make([]*x509.Certificate, 0)
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ for {
+ pemBlock, contents = pem.Decode(contents)
+ if pemBlock == nil {
+ return certs, nil
+ }
+
+ if pemBlock.Type == "CERTIFICATE" {
+ cert, err := x509.ParseCertificate(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+
+ certs = append(certs, cert)
+ }
+ }
+}
+
+func LoadPrivateKeyFromPEM(path string) (crypto.PrivateKey, error) {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ return LoadPrivateKeyFromPEMBytes(contents)
+}
+
+func LoadPrivateKeyFromPEMBytes(contents []byte) (crypto.PrivateKey, error) {
+ pemBlock, _ := pem.Decode(contents)
+ if pemBlock == nil {
+ return nil, fmt.Errorf("file contents do not contain PEM")
+ }
+
+ switch pemBlock.Type {
+ case "RSA PRIVATE KEY":
+ parsed, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ return parsed, nil
+ case "EC PRIVATE KEY":
+ parsed, err := x509.ParseECPrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ return parsed, nil
+ case "PRIVATE KEY":
+ parsed, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ return parsed, nil
+ }
+
+ return nil, fmt.Errorf("unsupported PEM block type: %s", pemBlock.Type)
+}
+
+func genericizePrivateKeyFunc[T crypto.PrivateKey](parser func([]byte) (T, error)) func([]byte) (crypto.PrivateKey, error) {
+ return func(input []byte) (crypto.PrivateKey, error) {
+ result, err := parser(input)
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+ }
+}
+
+func LoadPrivateKeyFromBytes(contents []byte) (crypto.PrivateKey, error) {
+ parsers := []func([]byte) (crypto.PrivateKey, error){
+ genericizePrivateKeyFunc(x509.ParsePKCS1PrivateKey),
+ genericizePrivateKeyFunc(x509.ParseECPrivateKey),
+ genericizePrivateKeyFunc(x509.ParsePKCS8PrivateKey),
+ }
+
+ for _, parser := range parsers {
+ if parsed, err := parser(contents); err != nil {
+ return parsed, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to decode arbitrary bytes to any supported private key type")
+}
+
+func SpiffeUrlFromCertificate(cert *x509.Certificate) *url.URL {
+ if spiffe, err := url.Parse(cert.Subject.CommonName); err == nil && spiffe.Scheme == "spiffe" {
+ return spiffe
+ }
+
+ for _, ext := range cert.Extensions {
+ if ext.Critical || !ext.Id.Equal(oidSubjectAltName) {
+ continue
+ }
+
+ values := make([]asn1.RawValue, 0)
+ _, err := asn1.Unmarshal(ext.Value, &values)
+ if err == nil {
+ for _, rawValue := range values {
+ if rawValue.Class == 2 && rawValue.Tag == 6 {
+ san := string(rawValue.Bytes)
+ url, err := url.Parse(san)
+ if err == nil && url.Scheme == "spiffe" {
+ return url
+ }
+ }
+ }
+
+ continue
+ }
+
+ san := string(ext.Value)
+
+ if !strings.HasPrefix(san, "URI:") {
+ continue
+ }
+
+ url, err := url.Parse(san[4:])
+ if err == nil && url.Scheme == "spiffe" {
+ return url
+ }
+
+ }
+
+ return nil
+}
+
+func Fingerprint(cert *x509.Certificate, hash crypto.Hash) []byte {
+ hasher := hash.New()
+ defer hasher.Reset()
+ dest := make([]byte, 0)
+ hasher.Write(cert.Raw)
+ return hasher.Sum(dest)
+}
--- /dev/null
+package fsnotify
+
+import (
+ "path/filepath"
+ "sync"
+
+ "go.fuhry.dev/fsnotify"
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type NotifyFunc = func(string, fsnotify.Op)
+type Op = fsnotify.Op
+
+var (
+ Write = fsnotify.Write
+ Remove = fsnotify.Remove
+ Create = fsnotify.Create
+ Rename = fsnotify.Rename
+ Chmod = fsnotify.Chmod
+ Close = fsnotify.Close
+)
+
+var startWatcherMu sync.Mutex
+var gWatcher *fsnotify.Watcher
+var logger *log.Logger
+var pendingWrites *hashset.HashSet[string]
+var watched *hashset.HashSet[string]
+var watchHandlers map[string][]NotifyFunc
+
+func startWatcher() error {
+ startWatcherMu.Lock()
+ defer startWatcherMu.Unlock()
+ if gWatcher != nil {
+ return nil
+ }
+
+ logger = log.Default().WithPrefix("mtls/fsnotify")
+ watchHandlers = make(map[string][]NotifyFunc, 0)
+ pendingWrites = hashset.NewHashSet[string]()
+ watched = hashset.NewHashSet[string]()
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Panicf("failed to start new global watcher")
+ }
+
+ gWatcher = watcher
+ go watcherLoop()
+ return nil
+}
+
+func addWatch(path string) error {
+ startWatcherMu.Lock()
+ defer startWatcherMu.Unlock()
+
+ if watched.Contains(path) {
+ return nil
+ }
+
+ err := gWatcher.Add(path)
+ if err != nil {
+ return err
+ }
+ watched.Add(path)
+ return nil
+}
+
+func watcherLoop() {
+ defer gWatcher.Close()
+
+ for {
+ select {
+ case event := <-gWatcher.Events:
+ if event.Op == 0 || event.Name == "" {
+ continue
+ }
+ handleEvent(event)
+ case err := <-gWatcher.Errors:
+ if err != nil {
+ logger.Error(err)
+ }
+ }
+ }
+}
+
+func handleEvent(event fsnotify.Event) {
+ if handlers, ok := watchHandlers[event.Name]; ok {
+
+ if event.Has(Create) {
+ addWatch(event.Name)
+ } else if event.Has(Remove) {
+ // remove watchers from deleted files.
+ // we will still be notified if the file is re-created, via the watch
+ // on the file's parent directory
+ gWatcher.Remove(event.Name)
+ }
+
+ if event.Op == Write {
+ pendingWrites.Add(event.Name)
+ return
+ } else if event.Op == Close {
+ if !pendingWrites.Contains(event.Name) {
+ return
+ }
+
+ pendingWrites.Del(event.Name)
+ }
+
+ for _, handler := range handlers {
+ handler(event.Name, event.Op)
+ }
+ }
+}
+
+func NotifyPath(path string, callback NotifyFunc) error {
+ err := startWatcher()
+ if err != nil {
+ return err
+ }
+
+ logger.V(1).Debugf("adding watcher on file %s", path)
+ err = addWatch(path)
+ if err != nil {
+ return err
+ }
+
+ if _, ok := watchHandlers[path]; !ok {
+ watchHandlers[path] = make([]NotifyFunc, 0)
+ }
+ watchHandlers[path] = append(watchHandlers[path], callback)
+
+ dirPath := filepath.Dir(path)
+ if dirPath != path {
+ logger.V(2).Debugf("adding watcher to parent directory %s", dirPath)
+ err = addWatch(dirPath)
+ if err != nil {
+ logger.Warnf("failed to add watcher for %s parent directory %s: %v", path, dirPath, err)
+ }
+ }
+
+ return nil
+}
--- /dev/null
+package mtls
+
+import (
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "os/user"
+ "strings"
+ "time"
+
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type PrincipalClass int
+
+const (
+ InvalidPrincipal PrincipalClass = iota
+ ServicePrincipal
+ UserPrincipal
+ SSLCertificatePrincipal
+)
+
+func (c PrincipalClass) String() string {
+ switch c {
+ case ServicePrincipal:
+ return "service"
+ case UserPrincipal:
+ return "user"
+ case SSLCertificatePrincipal:
+ return "tls"
+ }
+
+ panic("invalid PrincipalClass")
+}
+
+const (
+ defaultDefaultIdentity = "host"
+)
+
+var (
+ defaultMtlsIdentity string
+ logger *log.Logger
+)
+
+type Identity interface {
+ CertificateProvider
+
+ Name() string
+ Class() PrincipalClass
+ Equals(Identity) bool
+ IsValid() bool
+}
+
+type substantiatedIdentity struct {
+ CertificateProvider
+}
+
+type identityLoaderFunc func(name string) (CertificateProvider, error)
+
+type identityDriver struct {
+ name string
+ load identityLoaderFunc
+}
+
+var identityDrivers []*identityDriver
+
+func registerIdentityDriver(name string, load identityLoaderFunc) {
+ driver := &identityDriver{
+ name: name,
+ load: load,
+ }
+
+ identityDrivers = append(identityDrivers, driver)
+}
+
+func (id *substantiatedIdentity) Name() string {
+ cert, err := id.CertificateProvider.LeafCertificate()
+ if err != nil {
+ log.Fatalf("substantiatedIdentity failed to get certificate: %v", err)
+ }
+
+ spiffe := certutil.SpiffeUrlFromCertificate(cert)
+ if spiffe != nil {
+ parts := strings.Split(spiffe.Path, "/")
+ if len(parts) != 3 {
+ log.Fatalf("spiffe url invalid: path must have exactly 3 parts: %s", spiffe.Path)
+ }
+ switch parts[1] {
+ case "service":
+ return parts[2]
+ case "user":
+ return parts[2]
+ default:
+ log.Fatalf("spiffe url invalid: unknown idClass: %s", parts[1])
+ }
+ }
+
+ log.Fatalf("unable to get spiffe identity from certificate: none found in subject or SAN")
+ return ""
+}
+
+func (id *substantiatedIdentity) Class() PrincipalClass {
+ cert, err := id.CertificateProvider.LeafCertificate()
+ if err != nil {
+ log.Fatalf("substantiatedIdentity failed to get certificate: %v", err)
+ }
+
+ spiffe := certutil.SpiffeUrlFromCertificate(cert)
+ if spiffe != nil {
+ parts := strings.Split(spiffe.Path, "/")
+ if len(parts) != 3 {
+ log.Fatalf("spiffe url invalid: path must have exactly 3 parts: %s", spiffe.Path)
+ }
+ switch parts[1] {
+ case "service":
+ return ServicePrincipal
+ case "user":
+ return UserPrincipal
+ default:
+ log.Fatalf("spiffe url invalid: unknown idClass: %s", parts[1])
+ }
+ }
+
+ log.Fatal("unable to get spiffe identity from certificate: none found in subject or SAN")
+ return InvalidPrincipal
+}
+
+func (id *substantiatedIdentity) IsValid() bool {
+ return identityIsValid(id.CertificateProvider)
+}
+
+func identityIsValid(id certificatePrimitive) bool {
+ cert, err := id.LeafCertificate()
+ if err != nil {
+ return false
+ }
+
+ if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
+ return false
+ }
+
+ pkey, err := id.PrivateKey()
+ if err != nil || pkey == nil {
+ return false
+ }
+
+ return true
+}
+
+func (id *substantiatedIdentity) Equals(other Identity) bool {
+ return identityEquals(id, other)
+}
+
+func identityEquals(a Identity, b Identity) bool {
+ return a.Name() == b.Name() && a.Class() == b.Class()
+}
+
+type stubIdentity struct {
+ *inaccessibleCertificate
+
+ name string
+ cls PrincipalClass
+}
+
+func (id *stubIdentity) Name() string {
+ return id.name
+}
+
+func (id *stubIdentity) Class() PrincipalClass {
+ return id.cls
+}
+
+func (id *stubIdentity) Equals(other Identity) bool {
+ return identityEquals(id, other)
+}
+
+func (id *stubIdentity) IsValid() bool {
+ return false
+}
+
+func NewServiceIdentity(service string) Identity {
+ for _, driver := range identityDrivers {
+ logger.V(1).Infof("trying driver %s to load service identity %s", driver.name, service)
+ identity, err := driver.load(service)
+
+ if err == nil {
+ subst := &substantiatedIdentity{
+ CertificateProvider: identity,
+ }
+ logger.V(2).Infof("driver %s reports it loaded identity %s:%s", driver.name, subst.Class().String(), subst.Name())
+
+ if subst.Name() == service && subst.Class() == ServicePrincipal {
+ logger.V(1).Noticef("successfully loaded %s(%s) with driver %s", subst.Class().String(), subst.Name(), driver.name)
+ return subst
+ } else {
+ logger.V(2).Warnf(
+ "driver %s successfully loaded a certificate, but it doesn't match what "+
+ "we expected: class %s (expected)/%s (got); name %s (expected)/%s (got)",
+ driver.name, ServicePrincipal.String(), subst.Class().String(),
+ service, subst.Name())
+ }
+ } else {
+ logger.V(2).Warnf("driver %s failed to load service identity %s: %+v", driver.name, service, err)
+ }
+ }
+
+ return &stubIdentity{
+ inaccessibleCertificate: &inaccessibleCertificate{},
+
+ name: service,
+ cls: ServicePrincipal,
+ }
+}
+
+func NewUserIdentity(username string) Identity {
+ for _, driver := range identityDrivers {
+ logger.V(1).Infof("trying driver %s to load service identity %s", driver.name, username)
+ identity, err := driver.load(username)
+ if err == nil {
+ subst := &substantiatedIdentity{
+ CertificateProvider: identity,
+ }
+ logger.V(2).Infof("driver %s reports it loaded identity %s:%s", driver.name, subst.Class().String(), subst.Name())
+
+ if subst.Name() == username && subst.Class() == UserPrincipal {
+ logger.V(1).Noticef("successfully loaded %s(%s) with driver %s", subst.Class().String(), subst.Name(), driver.name)
+ return subst
+ } else {
+ logger.V(2).Warnf(
+ "driver %s successfully loaded a certificate, but it doesn't match what "+
+ "we expected: class %s (expected)/%s (got); name %s (expected)/%s (got)",
+ driver.name, ServicePrincipal.String(), subst.Class().String(),
+ username, subst.Name())
+ }
+ } else {
+ logger.V(2).Warnf("driver %s failed to load service identity %s: %+v", driver.name, username, err)
+ }
+ }
+
+ return &stubIdentity{
+ inaccessibleCertificate: &inaccessibleCertificate{},
+
+ name: username,
+ cls: UserPrincipal,
+ }
+}
+
+func NewDefaultUserIdentity() (Identity, error) {
+ user, err := user.Current()
+ if err != nil {
+ return nil, err
+ }
+
+ return NewUserIdentity(user.Username), nil
+}
+
+type substantiatedSslCertificate struct {
+ CertificateProvider
+
+ certName string
+}
+
+func (id *substantiatedSslCertificate) Name() string {
+ return id.certName
+}
+
+func (id *substantiatedSslCertificate) Class() PrincipalClass {
+ return SSLCertificatePrincipal
+}
+
+func (id *substantiatedSslCertificate) IsValid() bool {
+ return identityIsValid(id.CertificateProvider)
+}
+
+func (id *substantiatedSslCertificate) Equals(other Identity) bool {
+ return identityEquals(id, other)
+}
+
+func NewSSLCertificate(certName string) Identity {
+ fileId, err := LoadSSLCertificateFromFilesystem(certName)
+ if err == nil {
+ subst := &substantiatedSslCertificate{
+ CertificateProvider: fileId,
+ certName: certName,
+ }
+
+ if subst.Name() == certName && subst.Class() == SSLCertificatePrincipal {
+ return subst
+ }
+ }
+
+ return &stubIdentity{
+ inaccessibleCertificate: &inaccessibleCertificate{},
+
+ name: certName,
+ cls: SSLCertificatePrincipal,
+ }
+}
+
+func init() {
+ flag.StringVar(&defaultMtlsIdentity, "mtls.id", defaultMtlsIdentity, "mTLS identity to use when not overridden by the application")
+
+ // identityCache = make(map[string]*serviceIdentity, 0)
+
+ logger = log.Default().WithPrefix("mtls")
+}
+
+func SetDefaultIdentity(ident string) {
+ defaultMtlsIdentity = ident
+}
+
+// DefaultIdentity returns the Identity specified in the `-mtls.id` argument to the executable.
+func DefaultIdentity() Identity {
+ if !flag.Parsed() {
+ panic("cannot get default identity before flags are parsed")
+ }
+
+ if defaultMtlsIdentity == "" {
+ userId, err := NewDefaultUserIdentity()
+ if err == nil && userId.IsValid() {
+ leafCert, _ := userId.LeafCertificate()
+ log.Default().Infof("found valid user certificate, using identity: %s", leafCert.Subject)
+ return userId
+ } else {
+ log.Default().V(2).Debugf("couldn't load a user identity: err: %+v", err)
+ }
+
+ return NewServiceIdentity(defaultDefaultIdentity)
+ }
+
+ return NewServiceIdentity(defaultMtlsIdentity)
+}
+
+func IdentityFromTLSConnectionState(state *tls.ConnectionState) (Identity, error) {
+ if state == nil {
+ return nil, fmt.Errorf("connectionState is nil")
+ }
+
+ for _, cert := range state.PeerCertificates {
+ spiffe := certutil.SpiffeUrlFromCertificate(cert)
+ if spiffe != nil {
+ parts := strings.Split(spiffe.Path, "/")
+ if len(parts) != 3 {
+ return nil, fmt.Errorf("spiffe url invalid: path must have exactly 3 parts: %s", spiffe.Path)
+ }
+ switch parts[1] {
+ case "service":
+ return NewServiceIdentity(parts[2]), nil
+ case "user":
+ return NewUserIdentity(parts[2]), nil
+ default:
+ return nil, fmt.Errorf("spiffe url invalid: unknown idClass: %s", parts[1])
+ }
+ }
+ }
+ return nil, fmt.Errorf("could not get spiffe url from any provided peer certificate")
+}
--- /dev/null
+package mtls
+
+import (
+ "crypto"
+ "crypto/tls"
+ "fmt"
+ "os"
+ "sync"
+
+ "github.com/ThalesIgnite/crypto11"
+ "go.fuhry.dev/runtime/constants"
+)
+
+const (
+ deviceTrustObjectLabel = "Device Identity"
+)
+
+var pkcs11ModulePaths = []string{
+ "/usr/lib/pkcs11/libtpm2_pkcs11.so",
+ "/usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so",
+}
+
+var crypto11Config *crypto11.Config = &crypto11.Config{
+ TokenLabel: constants.DeviceTrustTokenName,
+ LoginNotSupported: true,
+}
+
+var (
+ p11Global *p11
+ p11GlobalError error
+ p11GlobalOnce sync.Once
+)
+
+type p11 struct {
+ cHandle *crypto11.Context
+}
+
+func getGlobalP11() (*p11, error) {
+ p11GlobalOnce.Do(func() {
+ p11Global, p11GlobalError = NewP11()
+ })
+
+ return p11Global, p11GlobalError
+}
+
+func NewP11() (*p11, error) {
+ for _, p := range pkcs11ModulePaths {
+ if _, err := os.Stat(p); err == nil {
+ crypto11Config.Path = p
+ }
+ }
+ if crypto11Config.Path == "" {
+ return nil, fmt.Errorf("unable to stat tpm2 pkcs11 module at any known path: %v", pkcs11ModulePaths)
+ }
+
+ ctx, err := crypto11.Configure(crypto11Config)
+ if err != nil {
+ return nil, err
+ }
+
+ p := &p11{
+ cHandle: ctx,
+ }
+
+ return p, nil
+}
+
+func (p *p11) Close() {
+ p.cHandle.Close()
+}
+
+func (p *p11) GetCertificate() (*tls.Certificate, error) {
+ cert, err := p.cHandle.FindCertificate(nil, []byte(deviceTrustObjectLabel), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ privateKey, err := p.GetPrivateKey()
+ if err != nil {
+ return nil, err
+ }
+
+ tlsCert := &tls.Certificate{
+ Certificate: [][]byte{
+ cert.Raw,
+ },
+ PrivateKey: privateKey,
+ Leaf: cert,
+ }
+ return tlsCert, nil
+}
+
+func (p *p11) GetPrivateKey() (crypto.Signer, error) {
+ kp, err := p.cHandle.FindKeyPair(nil, []byte(deviceTrustObjectLabel))
+ if err != nil {
+ return nil, fmt.Errorf("while getting private key handle: %v", err)
+ }
+
+ return kp, nil
+}
--- /dev/null
+package mtls
+
+import (
+ "context"
+ "crypto"
+ "crypto/tls"
+ "crypto/x509"
+ "flag"
+ "fmt"
+ "os"
+ "path"
+ "sync"
+
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/mtls/fsnotify"
+)
+
+type FileBackedCertificate struct {
+ LeafPath string
+ IntermediatesPath string
+ PrivateKeyPath string
+ RootPath string
+
+ mu sync.Mutex
+
+ tlsConfig *tls.Config
+ leaf *x509.Certificate
+ ints []*x509.Certificate
+ pkey crypto.PrivateKey
+ root *x509.Certificate
+}
+
+type fileBackedRoots struct {
+ RootPath string
+ IntermediatesPath string
+
+ ints []*x509.Certificate
+ root []*x509.Certificate
+
+ mu sync.Mutex
+ initOnce sync.Once
+}
+
+const (
+ defaultMtlsRootPath = "/etc/ssl/mtls"
+)
+
+var (
+ mtlsRootPaths = []string{defaultMtlsRootPath}
+ sslCertsBaseDir = "/etc/ssl/private"
+
+ defaultRootCAFile string
+ defaultIntermediateCAFile string
+)
+
+func LoadServiceIdentityFromFilesystem(serviceIdentity string) (*FileBackedCertificate, error) {
+ var lastErr error
+ for _, path := range mtlsRootPaths {
+ c, err := newFileBackedCertificateFromBaseDir(path, serviceIdentity)
+ if err == nil {
+ return c, nil
+ }
+ lastErr = err
+ }
+
+ return nil, lastErr
+}
+
+func newFileBackedCertificateFromBaseDir(mtlsRootPath string, serviceIdentity string) (*FileBackedCertificate, error) {
+ certDirectory := path.Join(mtlsRootPath, serviceIdentity)
+
+ leafPath := path.Join(certDirectory, "cert.pem")
+ chainPath := path.Join(certDirectory, "chain.pem")
+ keyPath := path.Join(certDirectory, "privkey.pem")
+ rootPath := path.Join(mtlsRootPath, "rootca.pem")
+
+ for _, file := range []string{leafPath, chainPath, keyPath, rootPath} {
+ if err := fileExistsAndIsReadable(file); err != nil {
+ return nil, err
+ }
+ }
+
+ return &FileBackedCertificate{
+ LeafPath: leafPath,
+ IntermediatesPath: chainPath,
+ PrivateKeyPath: keyPath,
+ RootPath: rootPath,
+ }, nil
+}
+
+func LoadUserIdentityFromFilesystem() (*FileBackedCertificate, error) {
+ fullChainPath, ok := os.LookupEnv("STEP_PERSONAL_CERTIFICATE")
+ if !ok {
+ return nil, fmt.Errorf("failed to get user certificate path from env STEP_PERSONAL_CERTIFICATE")
+ }
+
+ keyPath, ok := os.LookupEnv("STEP_PERSONAL_PRIVATE_KEY")
+ if !ok {
+ return nil, fmt.Errorf("failed to get user private key path from env STEP_PERSONAL_PRIVATE_KEY")
+ }
+
+ rootPath := path.Join(defaultMtlsRootPath, "rootca.pem")
+
+ for _, file := range []string{fullChainPath, keyPath, rootPath} {
+ if err := fileExistsAndIsReadable(file); err != nil {
+ return nil, err
+ }
+ }
+
+ return &FileBackedCertificate{
+ LeafPath: fullChainPath,
+ IntermediatesPath: fullChainPath,
+ PrivateKeyPath: keyPath,
+ RootPath: rootPath,
+ }, nil
+}
+
+func LoadSSLCertificateFromFilesystem(certName string) (*FileBackedCertificate, error) {
+ certDirectory := path.Join(defaultMtlsRootPath, certName)
+
+ leafPath := path.Join(certDirectory, "cert.pem")
+ chainPath := path.Join(certDirectory, "chain.pem")
+ keyPath := path.Join(certDirectory, "privkey.pem")
+ // FIXME!!
+ rootPath := "/etc/ssl/certs/ISRG_Root_X1.pem"
+
+ for _, file := range []string{leafPath, chainPath, keyPath, rootPath} {
+ if err := fileExistsAndIsReadable(file); err != nil {
+ return nil, err
+ }
+ }
+
+ return &FileBackedCertificate{
+ LeafPath: leafPath,
+ IntermediatesPath: chainPath,
+ PrivateKeyPath: keyPath,
+ RootPath: rootPath,
+ }, nil
+}
+
+func (c *FileBackedCertificate) LeafCertificate() (*x509.Certificate, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.leaf != nil {
+ return c.leaf, nil
+ }
+
+ leaf, err := c.tryLoadLeaf()
+ if err != nil {
+ return nil, err
+ }
+
+ c.leaf = leaf
+ return leaf, nil
+}
+
+func (c *FileBackedCertificate) tryLoadLeaf() (*x509.Certificate, error) {
+ certs, err := certutil.LoadCertificatesFromPEM(c.LeafPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range certs {
+ if cert.IsCA {
+ continue
+ }
+
+ return cert, nil
+ }
+
+ return nil, fmt.Errorf("leaf certificate path %q contains no end-entity certificates", c.LeafPath)
+}
+
+func (c *FileBackedCertificate) PrivateKey() (crypto.PrivateKey, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.pkey != nil {
+ return c.pkey, nil
+ }
+
+ pkey, err := c.tryLoadPrivateKey()
+ if err != nil {
+ return nil, err
+ }
+
+ c.pkey = pkey
+ return c.pkey, nil
+}
+
+func (c *FileBackedCertificate) tryLoadPrivateKey() (crypto.PrivateKey, error) {
+ return certutil.LoadPrivateKeyFromPEM(c.PrivateKeyPath)
+}
+
+func (c *FileBackedCertificate) RootCertificate() (*x509.Certificate, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.root != nil {
+ return c.root, nil
+ }
+
+ root, err := c.tryLoadRoot()
+ if err != nil {
+ return nil, err
+ }
+
+ c.root = root
+ return root, nil
+}
+
+func (c *FileBackedCertificate) tryLoadRoot() (*x509.Certificate, error) {
+ certs, err := certutil.LoadCertificatesFromPEM(c.RootPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range certs {
+ if cert.IsCA && cert.Issuer.ToRDNSequence().String() == cert.Subject.ToRDNSequence().String() {
+ return cert, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to find any self-signed certificates in root certificate file %q", c.RootPath)
+}
+
+func (c *FileBackedCertificate) IntermediateCertificates() ([]*x509.Certificate, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.ints != nil {
+ return c.ints, nil
+ }
+
+ newInts, err := c.tryLoadIntermediates()
+ if err != nil {
+ return nil, err
+ }
+
+ c.ints = newInts
+ return c.ints, nil
+}
+
+func (c *FileBackedCertificate) tryLoadIntermediates() ([]*x509.Certificate, error) {
+ certs, err := certutil.LoadCertificatesFromPEM(c.IntermediatesPath)
+ if err != nil {
+ return nil, err
+ }
+ newInts := make([]*x509.Certificate, 0)
+
+ for _, cert := range certs {
+ if !cert.IsCA {
+ continue
+ }
+
+ newInts = append(newInts, cert)
+ }
+
+ // don't check length of newInts here - sometimes there may not be any
+ return newInts, nil
+}
+
+func (r *fileBackedRoots) init() {
+ r.initOnce.Do(func() {
+ fsnotify.NotifyPath(r.RootPath, r.notifyEvent)
+ fsnotify.NotifyPath(r.IntermediatesPath, r.notifyEvent)
+ })
+}
+
+func (r *fileBackedRoots) RootCertificates() ([]*x509.Certificate, error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.init()
+
+ if r.root != nil {
+ return r.root, nil
+ }
+
+ root, err := r.tryLoadRoots()
+ if err != nil {
+ return nil, err
+ }
+
+ r.root = root
+ return r.root, nil
+}
+
+func (r *fileBackedRoots) tryLoadRoots() ([]*x509.Certificate, error) {
+ certs, err := certutil.LoadCertificatesFromPEM(r.RootPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range certs {
+ if cert.IsCA && cert.Issuer.String() == cert.Subject.String() {
+ return []*x509.Certificate{cert}, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to find any self-signed certificates in root certificate file %q", r.RootPath)
+}
+
+func (r *fileBackedRoots) IntermediateCertificates() ([]*x509.Certificate, error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.init()
+
+ if r.ints != nil {
+ return r.ints, nil
+ }
+
+ newInts, err := r.tryLoadIntermediates()
+ if err != nil {
+ return nil, err
+ }
+
+ r.ints = newInts
+ return r.ints, nil
+}
+
+func (r *fileBackedRoots) tryLoadIntermediates() ([]*x509.Certificate, error) {
+ certs, err := certutil.LoadCertificatesFromPEM(r.IntermediatesPath)
+ if err != nil {
+ return nil, err
+ }
+ newInts := make([]*x509.Certificate, 0)
+
+ for _, cert := range certs {
+ if !cert.IsCA {
+ continue
+ }
+
+ newInts = append(newInts, cert)
+ }
+
+ // don't check length of newInts here - sometimes there may not be any
+ return newInts, nil
+}
+
+func (r *fileBackedRoots) notifyEvent(filePath string, op fsnotify.Op) {
+ if op.Has(fsnotify.Remove) {
+ return
+ }
+
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ switch filePath {
+ case r.IntermediatesPath:
+ ints, err := r.tryLoadIntermediates()
+ if err == nil {
+ logger.Infof("detected change to intermediate certificates %s, reloaded", r.IntermediatesPath)
+ r.ints = ints
+ } else {
+ logger.Warningf("intermediate certificates %s changed but unable to reload: %v", r.IntermediatesPath, err)
+ }
+ case r.RootPath:
+ root, err := r.tryLoadRoots()
+ if err == nil {
+ logger.Infof("detected change to root certificate %s, reloaded", r.RootPath)
+ r.root = root
+ } else {
+ logger.Warningf("root certificate %s changed but unable to reload: %v", r.RootPath, err)
+ }
+ }
+}
+
+func (c *FileBackedCertificate) TlsConfig(ctx context.Context) (*tls.Config, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.tlsConfig != nil {
+ return c.tlsConfig, nil
+ }
+
+ fsnotify.NotifyPath(c.LeafPath, c.notifyEvent)
+ fsnotify.NotifyPath(c.IntermediatesPath, c.notifyEvent)
+ fsnotify.NotifyPath(c.PrivateKeyPath, c.notifyEvent)
+ fsnotify.NotifyPath(c.RootPath, c.notifyEvent)
+
+ c.tlsConfig = &tls.Config{
+ GetCertificate: c.GetCertificate,
+ GetClientCertificate: c.GetClientCertificate,
+ }
+
+ return c.tlsConfig, nil
+}
+
+func (c *FileBackedCertificate) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ return c.newTlsCertificate()
+}
+
+func (c *FileBackedCertificate) GetClientCertificate(reqInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+ return c.newTlsCertificate()
+}
+
+func (c *FileBackedCertificate) newTlsCertificate() (*tls.Certificate, error) {
+ return newTlsCertificate(c)
+}
+
+func (c *FileBackedCertificate) NewDialContextFunc() DialContextFunc {
+ return newDialContextFunc(c)
+}
+
+func (c *FileBackedCertificate) notifyEvent(filePath string, op fsnotify.Op) {
+ if op.Has(fsnotify.Remove) {
+ return
+ }
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ switch filePath {
+ case c.LeafPath:
+ cert, err := c.tryLoadLeaf()
+ if err == nil {
+ logger.Infof("detected change to leaf certificate %s, reloaded", c.LeafPath)
+ c.leaf = cert
+ } else {
+ logger.Warningf("leaf certificate %s changed but unable to reload: %v", c.LeafPath, err)
+ }
+ case c.IntermediatesPath:
+ ints, err := c.tryLoadIntermediates()
+ if err == nil {
+ logger.Infof("detected change to intermediate certificates %s, reloaded", c.IntermediatesPath)
+ c.ints = ints
+ } else {
+ logger.Warningf("intermediate certificates %s changed but unable to reload: %v", c.IntermediatesPath, err)
+ }
+ case c.PrivateKeyPath:
+ pkey, err := c.tryLoadPrivateKey()
+ if err == nil {
+ logger.Infof("detected change to private key %s, reloaded", c.PrivateKeyPath)
+ c.pkey = pkey
+ } else {
+ logger.Warningf("private key %s changed but unable to reload: %v", c.PrivateKeyPath, err)
+ }
+ case c.RootPath:
+ root, err := c.tryLoadRoot()
+ if err == nil {
+ logger.Infof("detected change to root certificate %s, reloaded", c.RootPath)
+ c.root = root
+ } else {
+ logger.Warningf("root certificate %s changed but unable to reload: %v", c.RootPath, err)
+ }
+ }
+}
+
+func fileExistsAndIsReadable(path string) error {
+ stat, err := os.Stat(path)
+ if err != nil {
+ return err
+ }
+ if !stat.Mode().IsRegular() {
+ return fmt.Errorf("%q is not a regular file", path)
+ }
+ fp, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ fp.Close()
+
+ return nil
+}
+
+func appendMtlsCertificateDir(path string) error {
+ stat, err := os.Stat(path)
+ if err != nil {
+ return err
+ }
+ if !stat.IsDir() {
+ return fmt.Errorf("mTLS certificate path is not a directory: %q", path)
+ }
+
+ mtlsRootPaths = append(mtlsRootPaths, path)
+ return nil
+}
+
+func init() {
+ defaultRootCAFile = fmt.Sprintf("%s/rootca.pem", defaultMtlsRootPath)
+ defaultIntermediateCAFile = fmt.Sprintf("%s/ca.pem", defaultMtlsRootPath)
+
+ defaultFileBackedRoots := &fileBackedRoots{}
+
+ if homeDir := os.Getenv("HOME"); homeDir != "" {
+ userMtlsPath := path.Join(homeDir, ".cache", "mtls")
+ appendMtlsCertificateDir(userMtlsPath)
+ }
+
+ flag.StringVar(&defaultFileBackedRoots.RootPath, "mtls.root-ca", defaultRootCAFile, "root CA file for verifying mTLS connections")
+ flag.StringVar(&defaultFileBackedRoots.IntermediatesPath, "mtls.intermediate-ca", defaultIntermediateCAFile, "intermediate CA file for verifying TLS connections")
+
+ flag.StringVar(&sslCertsBaseDir, "tls.certs-dir", sslCertsBaseDir, "directory to look under for public-site SSL certificates (NOT mTLS certs)")
+ flag.Func("mtls.certs-dir", "additional directory to search for mTLS certificates", appendMtlsCertificateDir)
+
+ registerIdentityDriver("file_service_global", func(serviceName string) (CertificateProvider, error) {
+ return LoadServiceIdentityFromFilesystem(serviceName)
+ })
+ registerIdentityDriver("file_user_home", func(_ string) (CertificateProvider, error) {
+ return LoadUserIdentityFromFilesystem()
+ })
+ registerRootDriver("file_etc_mtls", func() (rootsPrimitive, error) {
+ return defaultFileBackedRoots, nil
+ })
+}
--- /dev/null
+package mtls
+
+import (
+ "context"
+ "crypto"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "net"
+)
+
+type DialContextFunc func(context.Context, string, string) (net.Conn, error)
+
+type CertificateProvider interface {
+ certificatePrimitive
+
+ TlsConfig(context.Context) (*tls.Config, error)
+ NewDialContextFunc() DialContextFunc
+}
+
+type rootsPrimitive interface {
+ RootCertificates() ([]*x509.Certificate, error)
+ IntermediateCertificates() ([]*x509.Certificate, error)
+}
+
+type certificatePrimitive interface {
+ RootCertificate() (*x509.Certificate, error)
+ IntermediateCertificates() ([]*x509.Certificate, error)
+
+ LeafCertificate() (*x509.Certificate, error)
+ PrivateKey() (crypto.PrivateKey, error)
+
+ newTlsCertificate() (*tls.Certificate, error)
+}
+
+type inaccessibleCertificate struct{}
+
+var ErrCertificateInaccessible = errors.New("requested certificate is inaccessible")
+
+func (c *inaccessibleCertificate) LeafCertificate() (*x509.Certificate, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) IntermediateCertificates() ([]*x509.Certificate, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) RootCertificate() (*x509.Certificate, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) PrivateKey() (crypto.PrivateKey, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) newTlsCertificate() (*tls.Certificate, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) TlsConfig(ctx context.Context) (*tls.Config, error) {
+ return nil, ErrCertificateInaccessible
+}
+
+func (c *inaccessibleCertificate) NewDialContextFunc() DialContextFunc {
+ return newDialContextFunc(c)
+}
--- /dev/null
+//go:build darwin
+
+package mtls
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/keybase/go-keychain"
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/log"
+ "go.fuhry.dev/runtime/utils/stringmatch"
+)
+
+type macosKeychainCertificate struct {
+ certificatePrimitive
+
+ ints []*x509.Certificate
+ root *x509.Certificate
+ leaf *x509.Certificate
+ pkey crypto.PrivateKey
+}
+
+type macosKeychainRoots struct{}
+
+type kcCertResult struct {
+ cert *x509.Certificate
+ skid []byte
+}
+
+type kcCryptoPrivateKey interface {
+ Public() crypto.PublicKey
+ Equals(crypto.PrivateKey) bool
+}
+
+type kcCryptoPublicKey interface {
+ Equals(crypto.PublicKey) bool
+}
+
+type kcCryptoSigner struct {
+ pub crypto.PublicKey
+ keyRef *keychain.KeyRef
+}
+
+type kcSignerOpts struct {
+ hash crypto.Hash
+}
+
+func (kso *kcSignerOpts) HashFunc() crypto.Hash {
+ return kso.hash
+}
+
+var kcLogger *log.Logger
+
+func init() {
+ kcLogger = log.WithPrefix("mtls.macOSKeychain")
+
+ registerIdentityDriver("macos_keychain", NewCertificateFromMacKeychain)
+}
+
+func NewCertificateFromMacKeychain(principal string) (CertificateProvider, error) {
+ root, err := getMtlsRootFromMacKeychain()
+ if err != nil {
+ return nil, err
+ }
+ kcLogger.V(2).Debugf("loaded root cert from keychain: %s", root.Subject.String())
+
+ ints, err := getMtlsIntermediatesFromMacKeychain()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, c := range ints {
+ kcLogger.V(2).Debugf("loaded intermediate cert from keychain: %s", c.Subject.String())
+ }
+
+ leaves, err := getLeafCertificatesFromKeychainMatchingPrincipal(ServicePrincipal, principal)
+ if err != nil {
+ return nil, err
+ }
+
+ leaf, pkey, err := findCertificateAndPrivateKeyMatchingKeyPairInKeychain(leaves)
+ if err != nil {
+ return nil, err
+ }
+
+ c := &macosKeychainCertificate{
+ root: root,
+ ints: ints,
+ leaf: leaf,
+ pkey: pkey,
+ }
+
+ return c, nil
+}
+
+func (c *macosKeychainCertificate) LeafCertificate() (*x509.Certificate, error) {
+ return c.leaf, nil
+}
+
+func (c *macosKeychainCertificate) IntermediateCertificates() ([]*x509.Certificate, error) {
+ return c.ints, nil
+}
+
+func (c *macosKeychainCertificate) RootCertificate() (*x509.Certificate, error) {
+ return c.root, nil
+}
+
+func (c *macosKeychainCertificate) PrivateKey() (crypto.PrivateKey, error) {
+ return c.pkey, nil
+}
+
+func (c *macosKeychainCertificate) newTlsCertificate() (*tls.Certificate, error) {
+ return newTlsCertificate(c)
+}
+
+func (c *macosKeychainCertificate) NewDialContextFunc() DialContextFunc {
+ return newDialContextFunc(c)
+}
+
+func (c *macosKeychainCertificate) TlsConfig(ctx context.Context) (*tls.Config, error) {
+ tc := &tls.Config{
+ GetCertificate: c.GetCertificate,
+ GetClientCertificate: c.GetClientCertificate,
+ }
+
+ return tc, nil
+}
+
+func (c *macosKeychainCertificate) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ return c.newTlsCertificate()
+}
+
+func (c *macosKeychainCertificate) GetClientCertificate(reqInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+ return c.newTlsCertificate()
+}
+
+func getMtlsIntermediatesFromMacKeychain() ([]*x509.Certificate, error) {
+ certs, err := findCertificatesInKeychainWithLabel(stringmatch.Prefix(constants.IntCAName))
+ if err != nil {
+ return nil, err
+ }
+
+ ints := make([]*x509.Certificate, 0)
+ dupes := hashset.NewHashSet[string]()
+ for _, cert := range certs {
+ if cert.IsCA && cert.Subject.String() != cert.Issuer.String() {
+ fpr := hex.EncodeToString(certutil.Fingerprint(cert, crypto.SHA256))
+ if dupes.Contains(fpr) {
+ continue
+ }
+ dupes.Add(fpr)
+ ints = append(ints, cert)
+ }
+ }
+
+ return ints, nil
+}
+
+func getMtlsRootFromMacKeychain() (*x509.Certificate, error) {
+ certs, err := findCertificatesInKeychainWithLabel(stringmatch.Contains(constants.RootCAName))
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range certs {
+ if cert.IsCA && cert.Issuer.String() == cert.Subject.String() {
+ return cert, nil
+ }
+ }
+
+ return nil, errors.New("failed to find any root certificates in keychain")
+}
+
+func findCertificatesInKeychainWithLabel(match stringmatch.StringMatcher) ([]*x509.Certificate, error) {
+ parser := func(res *keychain.QueryResult) (*x509.Certificate, error) {
+ return x509.ParseCertificate(res.Data)
+ }
+ return searchKeychain(match, keychain.SecClassCertificate, parser)
+}
+
+func findCertificateAndPrivateKeyMatchingKeyPairInKeychain(leaves []*kcCertResult) (*x509.Certificate, crypto.Signer, error) {
+ matches := make([]stringmatch.StringMatcher, 0)
+ for _, leaf := range leaves {
+ matches = append(matches, stringmatch.Exact(leaf.cert.Subject.CommonName))
+ }
+ match := stringmatch.Or(matches...)
+
+ type matchingPair struct {
+ cert *x509.Certificate
+ pkey crypto.Signer
+ }
+
+ pairs, err := searchKeychain(
+ match,
+ keychain.SecClassCryptoKey,
+ func(res *keychain.QueryResult) (*matchingPair, error) {
+ if !res.HasKey {
+ return nil, fmt.Errorf("result does not contain a key")
+ }
+ if len(res.KeyLabel) != 20 {
+ return nil, fmt.Errorf("KeyLabel not populated")
+ }
+
+ for _, leaf := range leaves {
+ if len(leaf.skid) != 20 {
+ continue
+ }
+ if bytes.Equal(res.KeyLabel, leaf.skid) {
+ kcLogger.V(3).Noticef("found key %q matching desired subjectKeyId %s", res.Label, strings.ToUpper(hex.EncodeToString(leaf.skid)))
+
+ kcs := &kcCryptoSigner{
+ pub: leaf.cert.PublicKey,
+ keyRef: res.Key,
+ }
+
+ return &matchingPair{
+ cert: leaf.cert,
+ pkey: kcs,
+ }, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to identify any private key/certificate pairs")
+ })
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if len(pairs) < 1 {
+ return nil, nil, fmt.Errorf("no certificate/private key matching pairs found")
+ }
+
+ pair := pairs[0]
+ return pair.cert, pair.pkey, nil
+}
+
+// Public implements crypto.Decrypter and crypto.Signer.
+func (kcs *kcCryptoSigner) Public() crypto.PublicKey {
+ if kcs.pub != nil {
+ return kcs.pub
+ }
+
+ pub, err := kcs.keyRef.Public()
+ if err != nil {
+ kcLogger.Panicf("failed to convert keychain key to public key: %v", err)
+ return nil
+ }
+
+ if rsaPub, err := x509.ParsePKCS1PublicKey(pub); err == nil {
+ kcs.pub = rsaPub
+ return rsaPub
+ }
+ if pkixPub, err := x509.ParsePKIXPublicKey(pub); err == nil {
+ kcs.pub = pkixPub
+ return pkixPub
+ }
+
+ out := bytes.NewBufferString("")
+ encoder := base64.NewEncoder(base64.StdEncoding, out)
+ encoder.Write(pub)
+ pubB64 := out.String()
+ kcLogger.Panicf("public key was exported from keychain successfully, but couldn't be parsed: %s", pubB64)
+ return nil
+}
+
+// Sign implements crypto.Signer.
+func (kcs *kcCryptoSigner) Sign(randReader io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
+ alg, err := kcs.getSignerAlgo(opts)
+ if err != nil {
+ return nil, err
+ }
+ signed, err := kcs.keyRef.SignWithAlgorithm(digest, alg)
+ if err != nil {
+ kcLogger.Errorf("while signing using keychain provider: ", err)
+ return nil, err
+ }
+
+ return signed, nil
+}
+
+// Decrypt implements crypto.Decrypter.
+func (kcs *kcCryptoSigner) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) ([]byte, error) {
+ dec, err := kcs.keyRef.Decrypt(msg, kcs.Public())
+ if err != nil {
+ kcLogger.Error(err)
+ return nil, err
+ }
+ kcLogger.Debugf("decrypt ok: %+v", dec)
+ return dec, nil
+}
+
+func (kcs *kcCryptoSigner) getSignerAlgo(opts crypto.SignerOpts) (keychain.SecKeyAlgorithm, error) {
+ var err error
+ var algo keychain.SecKeyAlgorithm
+ hash := opts.HashFunc()
+ switch kcs.Public().(type) {
+ case *ecdsa.PublicKey:
+ switch hash {
+ case crypto.SHA1:
+ algo = keychain.ECDSASignatureDigestX962SHA1
+ case crypto.SHA256:
+ algo = keychain.ECDSASignatureDigestX962SHA256
+ case crypto.SHA384:
+ algo = keychain.ECDSASignatureDigestX962SHA384
+ case crypto.SHA512:
+ algo = keychain.ECDSASignatureDigestX962SHA512
+ default:
+ err = fmt.Errorf("unsupported hash")
+ }
+ case *rsa.PublicKey:
+ switch opts.(type) {
+ case *rsa.PSSOptions:
+ switch hash {
+ case crypto.SHA256:
+ algo = keychain.RSASignatureDigestPSSSHA256
+ case crypto.SHA384:
+ algo = keychain.RSASignatureDigestPSSSHA384
+ case crypto.SHA512:
+ algo = keychain.RSASignatureDigestPSSSHA512
+ default:
+ err = fmt.Errorf("unsupported hash")
+ }
+ default:
+ switch hash {
+ case crypto.SHA1:
+ algo = keychain.RSASignatureDigestPKCS1v15SHA1
+ case crypto.SHA256:
+ algo = keychain.RSASignatureDigestPKCS1v15SHA256
+ case crypto.SHA384:
+ algo = keychain.RSASignatureDigestPKCS1v15SHA384
+ case crypto.SHA512:
+ algo = keychain.RSASignatureDigestPKCS1v15SHA512
+ default:
+ err = fmt.Errorf("unsupported hash")
+ }
+ }
+ default:
+ err = fmt.Errorf("unsupported key type: %T", kcs.Public())
+ }
+
+ return algo, err
+}
+
+func getLeafCertificatesFromKeychainMatchingPrincipal(class PrincipalClass, principal string) ([]*kcCertResult, error) {
+ matcher := stringmatch.And(
+ stringmatch.Prefix("spiffe://"),
+ stringmatch.Suffix(fmt.Sprintf("/%s/%s", class.String(), principal)),
+ )
+
+ return searchKeychain(
+ matcher,
+ keychain.SecClassCertificate,
+ func(res *keychain.QueryResult) (*kcCertResult, error) {
+ if len(res.SubjectKeyIdentifier) != 20 {
+ return nil, fmt.Errorf("skid not populated for this certificate")
+ }
+ cert, err := x509.ParseCertificate(res.Data)
+ if err != nil {
+ return nil, err
+ }
+ if cert.IsCA {
+ return nil, fmt.Errorf("certificate has CA:TRUE flag set")
+ }
+ now := time.Now()
+ if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
+ return nil, fmt.Errorf("certificate has expired or is not yet valid")
+ }
+ kcLogger.V(3).Debugf(
+ "appending certificate with SubjectKeyIdentifier %s",
+ strings.ToUpper(hex.EncodeToString(res.SubjectKeyIdentifier)))
+
+ cr := &kcCertResult{
+ cert: cert,
+ skid: bytes.Clone(res.SubjectKeyIdentifier),
+ }
+ return cr, nil
+ })
+}
+
+func searchKeychain[T any](match stringmatch.StringMatcher, secClass keychain.SecClass, parser func(*keychain.QueryResult) (T, error)) ([]T, error) {
+ query := keychain.NewItem()
+
+ if int(secClass) != 0 {
+ query.SetSecClass(secClass)
+ }
+ query.SetReturnAttributes(true)
+ switch secClass {
+ case keychain.SecClassCryptoKey, keychain.SecClassIdentity:
+ query.SetReturnRef(true)
+ query.SetReturnData(false)
+ default:
+ query.SetReturnRef(false)
+ query.SetReturnData(true)
+ }
+ query.SetMatchLimit(keychain.MatchLimitAll)
+
+ items, err := keychain.QueryItem(query)
+ if err != nil {
+ return nil, fmt.Errorf("while attempting to load mTLS root certificate from keychain: %v", err)
+ }
+
+ results := make([]T, 0)
+ for _, res := range items {
+ if !match.Match(res.Label) {
+ continue
+ }
+
+ parsed, err := parser(&res)
+ if err != nil {
+ kcLogger.V(2).Warningf("found matching item %q in keychain, but failed to parse: %v", res.Label, err)
+ continue
+ }
+
+ kcLogger.V(2).Infof("successfully parsed keychain item %q as %T", res.Label, parsed)
+ results = append(results, parsed)
+ }
+
+ if len(results) == 0 {
+ return nil, fmt.Errorf("no items with the label %+v could be found", match)
+ }
+
+ return results, nil
+}
+
+func (kcr *macosKeychainRoots) RootCertificates() ([]*x509.Certificate, error) {
+ root, err := getMtlsRootFromMacKeychain()
+ if err != nil {
+ return nil, err
+ }
+
+ return []*x509.Certificate{root}, nil
+}
+
+func (kcr *macosKeychainRoots) IntermediateCertificates() ([]*x509.Certificate, error) {
+ return getMtlsIntermediatesFromMacKeychain()
+}
+
+func init() {
+ registerRootDriver("macos_keychain", func() (rootsPrimitive, error) {
+ return &macosKeychainRoots{}, nil
+ })
+}
--- /dev/null
+package mtls
+
+import (
+ "context"
+ "crypto/tls"
+ "net"
+)
+
+func newTlsCertificate(id certificatePrimitive) (*tls.Certificate, error) {
+ leafCertificate, err := id.LeafCertificate()
+ if err != nil {
+ return nil, err
+ }
+ privateKey, err := id.PrivateKey()
+ if err != nil {
+ return nil, err
+ }
+ intermediates, err := id.IntermediateCertificates()
+ if err != nil {
+ return nil, err
+ }
+ rawCerts := make([][]byte, 0)
+ rawCerts = append(rawCerts, leafCertificate.Raw)
+ for _, c := range intermediates {
+ rawCerts = append(rawCerts, c.Raw)
+ }
+
+ return &tls.Certificate{
+ Certificate: rawCerts,
+ PrivateKey: privateKey,
+ Leaf: leafCertificate,
+ }, nil
+}
+
+func newDialContextFunc(id CertificateProvider) DialContextFunc {
+ dcf := func(ctx context.Context, network, addr string) (net.Conn, error) {
+ c, err := id.TlsConfig(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return tls.Dial(network, addr, c)
+ }
+
+ return dcf
+}
--- /dev/null
+//go:build linux
+
+package mtls
+
+import (
+ "context"
+ "crypto"
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "path"
+
+ "go.fuhry.dev/runtime/mtls/certutil"
+)
+
+type TPMBackedCertificate struct {
+ certificatePrimitive
+
+ p11 *p11
+}
+
+func NewTPMBackedCertificate() (*TPMBackedCertificate, error) {
+ pkcs, err := getGlobalP11()
+ if err != nil {
+ return nil, err
+ }
+
+ cert := &TPMBackedCertificate{
+ p11: &p11{
+ cHandle: pkcs.cHandle,
+ },
+ }
+
+ return cert, nil
+}
+
+func (c *TPMBackedCertificate) LeafCertificate() (*x509.Certificate, error) {
+ tlsCert, err := c.p11.GetCertificate()
+ if err != nil {
+ return nil, err
+ }
+
+ return tlsCert.Leaf, nil
+}
+
+func (c *TPMBackedCertificate) IntermediateCertificates() ([]*x509.Certificate, error) {
+ caFile := path.Join(defaultMtlsRootPath, "ca.pem")
+
+ certs, err := certutil.LoadCertificatesFromPEM(caFile)
+ if err != nil {
+ return nil, err
+ }
+
+ ints := make([]*x509.Certificate, 0)
+ for _, cert := range certs {
+ if cert.IsCA {
+ ints = append(ints, cert)
+ }
+ }
+
+ return ints, nil
+}
+
+func (c *TPMBackedCertificate) PrivateKey() (crypto.PrivateKey, error) {
+ pkey, err := c.p11.GetPrivateKey()
+ if err != nil {
+ return nil, err
+ }
+
+ return pkey, nil
+}
+
+func (c *TPMBackedCertificate) RootCertificate() (*x509.Certificate, error) {
+ caFile := path.Join(defaultMtlsRootPath, "ca.pem")
+
+ certs, err := certutil.LoadCertificatesFromPEM(caFile)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range certs {
+ if cert.IsCA && cert.Issuer.String() == cert.Subject.String() {
+ return cert, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to find any self-signed ca certificates in %s", caFile)
+}
+
+func (c *TPMBackedCertificate) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ return newTlsCertificate(c)
+}
+
+func (c *TPMBackedCertificate) getClientCertificate(reqInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+ return newTlsCertificate(c)
+}
+
+func (c *TPMBackedCertificate) TlsConfig(ctx context.Context) (*tls.Config, error) {
+ tlsConfig := &tls.Config{
+ GetCertificate: c.getCertificate,
+ GetClientCertificate: c.getClientCertificate,
+ }
+
+ return tlsConfig, nil
+}
+
+func (c *TPMBackedCertificate) NewDialContextFunc() DialContextFunc {
+ return newDialContextFunc(c)
+}
+
+func init() {
+ registerIdentityDriver("tpm2-pkcs11", func(_ string) (CertificateProvider, error) {
+ return NewTPMBackedCertificate()
+ })
+}
--- /dev/null
+package mtls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type IdentityClass uint
+
+type remoteIdentity struct {
+ class IdentityClass
+ domain string
+ princ string
+}
+
+const (
+ Domain IdentityClass = iota
+ Service
+ User
+ All
+)
+
+const (
+ exprSpiffeServiceIdentity = `^spiffe://(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/service/(?P<principal>[a-z0-9_-]+)$`
+ exprSpiffeUserIdentity = `^spiffe://(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/user/(?P<principal>[a-z0-9_-]+)$`
+ exprMtlsInternalIdenitty = `^(?P<principal>[A-Za-z0-9_-]+)\.(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)\.mtls\.internal$`
+)
+
+func (c IdentityClass) String() string {
+ switch c {
+ case Domain:
+ return "domain"
+ case Service:
+ return "service"
+ case User:
+ return "user"
+ case All:
+ return "all"
+ }
+
+ panic("invalid value for IdentityClass")
+}
+
+var (
+ spiffeServiceIdentity, spiffeUserIdentity, mtlsInternalIdentity *regexp.Regexp
+)
+
+func init() {
+ spiffeServiceIdentity = regexp.MustCompile(exprSpiffeServiceIdentity)
+ spiffeUserIdentity = regexp.MustCompile(exprSpiffeUserIdentity)
+ mtlsInternalIdentity = regexp.MustCompile(exprMtlsInternalIdenitty)
+}
+
+// MTLSPeerVerifier validates SPIFFE CNs and SANs of peer certificates, and facilitates
+// configuration of conventional certificate validation (chain, notbefore/notafter, key
+// usage, etc.) against the mTLS CA.
+//
+// It will not configure the local identity's certificates, private keys, etc. - for that,
+// use `Identity.TlsConfig()`.
+type MTLSPeerVerifier interface {
+ // ConfigureServer modifies a tls.Config struct to require client certificates, and verify
+ // the client certificate using the custom chain and name verifiers.
+ ConfigureServer(*tls.Config) error
+
+ // ConfigureClient modifies a tls.Config struct to verify server certificates against the
+ // mTLS root CA.
+ ConfigureClient(*tls.Config) error
+
+ // AllowFrom allow-lists one or more peer identities of a given type.
+ //
+ // Specify the IdentityClass "Any" to allow all clients.
+ //
+ // If IdentityClass is Service or User, the variadic arguments are the service names or
+ // usernames that are permitted to connect.
+ AllowFrom(IdentityClass, ...string)
+
+ // VerifyPeerCert conforms to the function prototype for `tls.Config.VerifyConnection`.
+ //
+ // This function is called after VerifyPeerCertificate completes without error.
+ VerifyPeerCert(*x509.Certificate, x509.VerifyOptions) error
+}
+
+type mtlsPeerVerifier struct {
+ MTLSPeerVerifier
+
+ allowedPrincipals map[IdentityClass]*hashset.HashSet[string]
+ log *log.Logger
+}
+
+func NewPeerNameVerifier() MTLSPeerVerifier {
+ cv := &mtlsPeerVerifier{
+ allowedPrincipals: make(map[IdentityClass]*hashset.HashSet[string]),
+ log: log.WithPrefix("MTLSPeerVerifier"),
+ }
+
+ return cv
+}
+
+func (cv *mtlsPeerVerifier) ConfigureServer(tlsConfig *tls.Config) error {
+ vo, err := NewMTLSVerifyOpts()
+ if err != nil {
+ return err
+ }
+ tlsConfig.VerifyPeerCertificate = NewVerifyMTLSPeerCertificateFuncWithOpts(vo)
+ tlsConfig.InsecureSkipVerify = true
+ tlsConfig.VerifyConnection = cv.verifyConnectionFunc(vo)
+ tlsConfig.ClientCAs = vo.Roots
+ tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
+
+ return nil
+}
+
+func (cv *mtlsPeerVerifier) ConfigureClient(tlsConfig *tls.Config) error {
+ vo, err := NewMTLSVerifyOpts()
+ if err != nil {
+ return err
+ }
+ tlsConfig.InsecureSkipVerify = true
+ tlsConfig.VerifyPeerCertificate = NewVerifyMTLSPeerCertificateFuncWithOpts(vo)
+ tlsConfig.VerifyConnection = cv.verifyConnectionFunc(vo)
+
+ return nil
+}
+
+func (cv *mtlsPeerVerifier) verifyConnectionFunc(verifyOpts x509.VerifyOptions) func(tls.ConnectionState) error {
+ return func(cs tls.ConnectionState) error {
+ if len(cs.PeerCertificates) < 1 {
+ return fmt.Errorf("no peer certificate provided")
+ }
+
+ peerCert := cs.PeerCertificates[0]
+
+ return cv.VerifyPeerCert(peerCert, verifyOpts)
+ }
+}
+
+func (cv *mtlsPeerVerifier) VerifyPeerCert(peerCert *x509.Certificate, verifyOpts x509.VerifyOptions) error {
+ _, err := peerCert.Verify(verifyOpts)
+ if err != nil {
+ return err
+ }
+ err = cv.checkName(peerCert.Subject.CommonName)
+ if err == nil {
+ cv.log.V(2).Infof("accepted peer certificate with CN = %q", peerCert.Subject.CommonName)
+ return nil
+ }
+ cv.log.V(2).Infof("did not accept peer certificate CN %q: %v", peerCert.Subject.CommonName, err)
+
+ for _, dnsName := range peerCert.DNSNames {
+ err = cv.checkName(dnsName)
+ if err == nil {
+ cv.log.V(1).Infof("accepted peer certificate with SAN = DNS:%q", dnsName)
+ return nil
+ }
+ cv.log.V(2).Infof("did not accept peer certificate dnsName %q: %v", dnsName, err)
+ }
+
+ for _, url := range peerCert.URIs {
+ err = cv.checkName(url.String())
+ if err == nil {
+ cv.log.V(1).Infof("accepted peer certificate with SAN = URI:%q", url.String())
+ return nil
+ }
+ cv.log.V(2).Infof("did not accept peer certificate URI %q: %v", url.String(), err)
+ }
+
+ cv.log.V(2).Errorf("rejected connection because no names were allowed")
+ return fmt.Errorf("none of the names in this certificate are allowed")
+}
+
+func (cv *mtlsPeerVerifier) AllowFrom(class IdentityClass, principals ...string) {
+ if len(principals) < 1 && class != All {
+ return
+ }
+ hs, ok := cv.allowedPrincipals[class]
+ if !ok {
+ cv.allowedPrincipals[class] = hashset.NewHashSet[string]()
+ hs = cv.allowedPrincipals[class]
+ }
+ for _, princ := range principals {
+ hs.Add(princ)
+ }
+}
+
+func (cv *mtlsPeerVerifier) checkName(name string) error {
+ id, err := parseName(name)
+ if err != nil {
+ return err
+ }
+ cv.log.V(3).Debugf("parsed name %q to %+v (class=%s)", name, id, id.class.String())
+
+ if _, ok := cv.allowedPrincipals[All]; ok {
+ return nil
+ }
+
+ if allowedDomains, ok := cv.allowedPrincipals[Domain]; ok {
+ if allowedDomains.Contains(id.domain) {
+ cv.log.V(3).Debugf("domain %q exactly matched allowlist", id.domain)
+ return nil
+ }
+ domainOk := false
+ for _, val := range allowedDomains.AsSlice() {
+ if val[0] != '.' {
+ continue
+ }
+ if strings.HasSuffix(id.domain, val) {
+ cv.log.V(3).Debugf("domain %q matched suffix rule %q", id.domain, val)
+ domainOk = true
+ break
+ }
+ }
+ if !domainOk {
+ return fmt.Errorf("trust domain %q is not allowed to authenticate to this service", id.domain)
+ }
+ }
+
+ if allowedPrincipals, ok := cv.allowedPrincipals[id.class]; ok {
+ if !allowedPrincipals.Contains(id.princ) {
+ return fmt.Errorf("principal %q is not allowed to authenticate to this service", id.princ)
+ }
+
+ cv.log.V(3).Debugf("principal %q matched allowlist", id.princ)
+ return nil
+ }
+
+ return fmt.Errorf("principals of this type are not allowed")
+}
+
+func parseName(name string) (*remoteIdentity, error) {
+ exps := []struct {
+ expr *regexp.Regexp
+ class IdentityClass
+ }{
+ {
+ expr: spiffeServiceIdentity,
+ class: Service,
+ },
+ {
+ expr: mtlsInternalIdentity,
+ class: Service,
+ },
+ {
+ expr: spiffeUserIdentity,
+ class: User,
+ },
+ }
+
+ for _, e := range exps {
+ if !e.expr.MatchString(name) {
+ continue
+ }
+
+ parts := e.expr.FindStringSubmatch(name)
+ iDomain, iPrinc := e.expr.SubexpIndex("domain"), e.expr.SubexpIndex("principal")
+ return &remoteIdentity{
+ class: e.class,
+ domain: parts[iDomain],
+ princ: parts[iPrinc],
+ }, nil
+ }
+
+ return nil, fmt.Errorf("cannot understand CN/SAN %q as an mTLS identity", name)
+}
--- /dev/null
+package mtls
+
+import (
+ "crypto/x509"
+ "errors"
+ "sync"
+ "time"
+
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type tlsVerifyPeerCertificatesFunc = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
+
+type rootDriver struct {
+ name string
+ load func() (rootsPrimitive, error)
+}
+
+func registerRootDriver(name string, load func() (rootsPrimitive, error)) {
+ if rootsDrivers == nil {
+ rootsDrivers = make([]*rootDriver, 0)
+ }
+
+ rd := &rootDriver{
+ name: name,
+ load: load,
+ }
+
+ rootsDrivers = append(rootsDrivers, rd)
+}
+
+var (
+ rootsDrivers []*rootDriver
+
+ mtlsVerifyOpts *x509.VerifyOptions
+ mtlsVerifyOptsOnce sync.Once
+
+ ErrCertificateParseFailed = errors.New("failed to parse a certificate that was presented in the handshake")
+ ErrMultipleCertificatesPresented = errors.New("peer presented multiple leaf certificates")
+ ErrNoCertificatePresented = errors.New("peer did not present a certificate")
+ ErrCertificateNotYetValid = errors.New("certificate is not yet valid")
+ ErrCertificateExpired = errors.New("certificate has expired")
+ ErrCertificateHasUnhandledCriticalExtensions = errors.New("certificate has unhandled critical extensions")
+ ErrBasicConstraintsInvalid = errors.New("leaf certificate basic constraints are invalid")
+ ErrCertificateCAFlagSet = errors.New("leaf certificate has CA:TRUE flag in basicConstraints")
+)
+
+func selectRoots() ([]*x509.Certificate, []*x509.Certificate, error) {
+ if rootsDrivers == nil {
+ return nil, nil, errors.New("no roots drivers were registered")
+ }
+
+ for _, driver := range rootsDrivers {
+ logger.V(1).Infof("asking roots driver %s for roots and intermediates", driver.name)
+
+ provider, err := driver.load()
+ if err != nil {
+ logger.V(2).Warningf("driver %s failed to initialize: %v", driver.name, err)
+ continue
+ }
+
+ root, err := provider.RootCertificates()
+ if err != nil {
+ logger.V(2).Warningf("driver %s (%T) failed to load root certificate: %v", driver.name, provider, err)
+ continue
+ }
+
+ ints, err := provider.IntermediateCertificates()
+ if err != nil {
+ logger.V(2).Warningf("driver %s (%T) failed to load intermediate certificates: %v", driver.name, provider, err)
+ continue
+ }
+
+ logger.V(1).Noticef("using driver %s (%T) for root and intermediate certificates", driver.name, provider)
+
+ return root, ints, nil
+ }
+
+ return nil, nil, errors.New("no usable roots driver was found")
+}
+
+func newMTLSVerifyOpts() (*x509.VerifyOptions, error) {
+ rootPool := x509.NewCertPool()
+ intermediatePool := x509.NewCertPool()
+
+ root, ints, err := selectRoots()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, cert := range root {
+ rootPool.AddCert(cert)
+ }
+
+ for _, cert := range ints {
+ intermediatePool.AddCert(cert)
+ }
+
+ opts := &x509.VerifyOptions{
+ Intermediates: intermediatePool,
+ Roots: rootPool,
+ KeyUsages: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageAny,
+ },
+ }
+
+ return opts, nil
+}
+
+func NewMTLSVerifyOpts() (x509.VerifyOptions, error) {
+ mtlsVerifyOptsOnce.Do(func() {
+ var err error
+ mtlsVerifyOpts, err = newMTLSVerifyOpts()
+ if err != nil {
+ log.Panicf("failed to load mtls root certificates: %v", err)
+ }
+ })
+
+ verifyOptsClone := x509.VerifyOptions{
+ Intermediates: mtlsVerifyOpts.Intermediates.Clone(),
+ Roots: mtlsVerifyOpts.Roots.Clone(),
+ KeyUsages: make([]x509.ExtKeyUsage, 0),
+ }
+
+ verifyOptsClone.KeyUsages = append(verifyOptsClone.KeyUsages, mtlsVerifyOpts.KeyUsages...)
+
+ return verifyOptsClone, nil
+}
+
+func checkLeafCertificateConstraints(leafCert *x509.Certificate) error {
+ if leafCert.NotBefore.After(time.Now()) {
+ return ErrCertificateNotYetValid
+ }
+ if leafCert.NotAfter.Before(time.Now()) {
+ return ErrCertificateExpired
+ }
+ if len(leafCert.UnhandledCriticalExtensions) > 0 {
+ return ErrCertificateHasUnhandledCriticalExtensions
+ }
+ // if !leafCert.BasicConstraintsValid {
+ // return ErrBasicConstraintsInvalid
+ // }
+ if leafCert.IsCA {
+ return ErrCertificateCAFlagSet
+ }
+ return nil
+}
+
+func verifyMTLSCertificateChain(leafCert *x509.Certificate, intermediates []*x509.Certificate, mtlsVerifyOpts x509.VerifyOptions) error {
+ for _, intCert := range intermediates {
+ mtlsVerifyOpts.Intermediates.AddCert(intCert)
+ }
+
+ chains, err := leafCert.Verify(mtlsVerifyOpts)
+ if err != nil {
+ return err
+ }
+
+ for _, chain := range chains {
+ lastCert := chain[0]
+ logger.Debugf("checking constraints on leaf certificate: %+v", lastCert.Subject.String())
+ if err := checkLeafCertificateConstraints(lastCert); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func NewVerifyMTLSPeerCertificateFunc() (tlsVerifyPeerCertificatesFunc, error) {
+ vo, err := NewMTLSVerifyOpts()
+ if err != nil {
+ return nil, err
+ }
+
+ return NewVerifyMTLSPeerCertificateFuncWithOpts(vo), nil
+}
+
+func NewVerifyMTLSPeerCertificateFuncWithOpts(vo x509.VerifyOptions) tlsVerifyPeerCertificatesFunc {
+ return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ var leafCert *x509.Certificate
+
+ intermediates := make([]*x509.Certificate, 0)
+
+ for _, rawCert := range rawCerts {
+ cert, err := x509.ParseCertificate(rawCert)
+ if err == nil {
+ if cert.IsCA {
+ intermediates = append(intermediates, cert)
+ } else {
+ if leafCert != nil {
+ return ErrMultipleCertificatesPresented
+ }
+ leafCert = cert
+ }
+ } else {
+ return ErrCertificateParseFailed
+ }
+ }
+
+ if leafCert == nil {
+ return ErrNoCertificatePresented
+ }
+
+ return verifyMTLSCertificateChain(leafCert, intermediates, vo)
+ }
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(GOSRC:.go=)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: %.go
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "flag"
+ "io"
+ "os"
+ "strings"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type flagStringSlice []string
+
+func (s *flagStringSlice) String() string {
+ return strings.Join([]string(*s), ", ")
+}
+
+func (s *flagStringSlice) Set(value string) error {
+ *s = append(*s, value)
+ return nil
+}
+
+func main() {
+ var services, users, domains flagStringSlice
+
+ var certPath string
+ var certContents []byte
+ var certParsed *x509.Certificate
+ var err error
+
+ flag.Var(&services, "service", "List of services to allow")
+ flag.Var(&users, "user", "List of users to allow")
+ flag.Var(&domains, "domain", "List of domains to allow")
+ flag.StringVar(&certPath, "certificate", "", "client certificate presented; if omitted, reads from standard input")
+
+ flag.Parse()
+
+ if certPath == "" {
+ certContents, err = io.ReadAll(os.Stdin)
+ if err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ certContents, err = os.ReadFile(certPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ pemValue, rest := pem.Decode(certContents)
+ if pemValue != nil {
+ if pemValue.Type != "CERTIFICATE" {
+ log.Fatal("PEM-encoded input is not a certificate")
+ }
+ certParsed, err = x509.ParseCertificate(pemValue.Bytes)
+ if err != nil {
+ log.Fatal("failed to parse the provided certificate", err)
+ }
+ } else {
+ certParsed, err = x509.ParseCertificate(rest)
+ if err != nil {
+ log.Fatal("failed to parse the provided certificate", err)
+ }
+ }
+
+ log.Default().Debugf("parsed certificate with subject: %+v", certParsed.Subject)
+
+ cv := mtls.NewPeerNameVerifier()
+ cv.AllowFrom(mtls.Service, services...)
+ cv.AllowFrom(mtls.User, users...)
+ cv.AllowFrom(mtls.Domain, domains...)
+
+ verifyOpts, err := mtls.NewMTLSVerifyOpts()
+ if err != nil {
+ log.Default().Warn("creating verify opts: ", err)
+ os.Exit(1)
+ }
+
+ err = cv.VerifyPeerCert(certParsed, verifyOpts)
+ if err != nil {
+ log.Default().Warn("certificate validation failed: ", err)
+ os.Exit(1)
+ }
+
+ os.Exit(0)
+}
--- /dev/null
+package dns
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/miekg/dns"
+)
+
+var dnsCache *lru.Cache
+var dnsCacheInit sync.Once
+
+func ResolveDualStack(hostname string) (string, string, error) {
+ var msg *dns.Msg
+ var err error
+
+ dnsCacheInit.Do(func() {
+ var err error
+ dnsCache, err = lru.New(256)
+ if err != nil {
+ panic(fmt.Errorf("failed to create lru cache: %v", err))
+ }
+ })
+
+ hostname = dns.Fqdn(hostname)
+
+ if entry, ok := dnsCache.Get(hostname); ok {
+ if msg, ok = entry.(*dns.Msg); !ok {
+ return "", "", fmt.Errorf("lru cache is corrupt: expected entry to be *dns.Msg, got %T", entry)
+ }
+ } else {
+ msg, err = doDualStackQuery(hostname)
+ if err != nil {
+ return "", "", err
+ }
+
+ dnsCache.Add(hostname, msg)
+ }
+
+ var (
+ ip4 string
+ ip6 string
+ )
+
+ for _, rr := range msg.Answer {
+ switch a := rr.(type) {
+ case *dns.A:
+ ip4 = a.A.String()
+ case *dns.AAAA:
+ ip6 = a.AAAA.String()
+ }
+ }
+
+ return ip4, ip6, nil
+}
+
+func doDualStackQuery(hostname string) (*dns.Msg, error) {
+ var msg *dns.Msg
+
+ cc, err := dns.ClientConfigFromFile("/etc/resolv.conf")
+ if err != nil {
+ return nil, err
+ }
+
+ for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
+ query := &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: dns.Id(),
+ RecursionDesired: true,
+ AuthenticatedData: true,
+ },
+ Question: make([]dns.Question, 1),
+ Extra: []dns.RR{
+ newEDNSCookie(),
+ },
+ }
+
+ query.Question[0] = dns.Question{
+ Name: hostname,
+ Qtype: qtype,
+ Qclass: dns.ClassINET,
+ }
+
+ mu := &sync.Mutex{}
+ done := false
+
+ client := &dns.Client{}
+ resultChan := make(chan *dns.Msg, len(cc.Servers))
+ defer (func() {
+ mu.Lock()
+ done = true
+ close(resultChan)
+ mu.Unlock()
+ })()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ for _, server := range cc.Servers {
+ go (func() {
+ m, _, err := client.Exchange(query, server+":53")
+ if m != nil && err == nil {
+ mu.Lock()
+ defer mu.Unlock()
+ if !done {
+ resultChan <- m
+ }
+ }
+ })()
+ }
+
+ select {
+ case m := <-resultChan:
+ if msg == nil {
+ msg = m
+ } else {
+ msg.Answer = append(msg.Answer, m.Answer...)
+ }
+ case <-ctx.Done():
+ return nil, fmt.Errorf("resolving name %s timed out", hostname)
+ }
+ }
+
+ return msg, nil
+}
+
+func newEDNSCookie() dns.RR {
+ clientCookie := fmt.Sprintf("%016x", rand.Uint64())
+ return &dns.OPT{
+ Hdr: dns.RR_Header{
+ Name: ".",
+ Rrtype: dns.TypeOPT,
+ },
+ Option: []dns.EDNS0{
+ &dns.EDNS0_COOKIE{
+ Code: dns.EDNS0COOKIE,
+ Cookie: clientCookie,
+ },
+ },
+ }
+}
--- /dev/null
+PROTO_SRCS := $(wildcard *.proto)
+PROTO_GO_OUTPUT := $(PROTO_SRCS:.proto=.pb.go) $(PROTO_SRCS:.proto=_grpc.pb.go)
+
+$(PROTO_GO_OUTPUT): $(PROTO_SRCS)
+ protoc --go_out=. --go_opt=paths=source_relative \
+ --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+ $(PROTO_SRCS)
+
+pb_go: $(PROTO_GO_OUTPUT)
+
+all: pb_go
+
+clean:
+ rm -fv $(PROTO_GO_OUTPUT)
--- /dev/null
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.30.0
+// protoc v4.25.1
+// source: attest_server.proto
+
+package attest
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type PCR_DigestAlg int32
+
+const (
+ PCR_INVALID_DIGEST PCR_DigestAlg = 0
+ PCR_SHA1 PCR_DigestAlg = 1
+ PCR_SHA256 PCR_DigestAlg = 2
+ PCR_SHA384 PCR_DigestAlg = 3
+)
+
+// Enum value maps for PCR_DigestAlg.
+var (
+ PCR_DigestAlg_name = map[int32]string{
+ 0: "INVALID_DIGEST",
+ 1: "SHA1",
+ 2: "SHA256",
+ 3: "SHA384",
+ }
+ PCR_DigestAlg_value = map[string]int32{
+ "INVALID_DIGEST": 0,
+ "SHA1": 1,
+ "SHA256": 2,
+ "SHA384": 3,
+ }
+)
+
+func (x PCR_DigestAlg) Enum() *PCR_DigestAlg {
+ p := new(PCR_DigestAlg)
+ *p = x
+ return p
+}
+
+func (x PCR_DigestAlg) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (PCR_DigestAlg) Descriptor() protoreflect.EnumDescriptor {
+ return file_attest_server_proto_enumTypes[0].Descriptor()
+}
+
+func (PCR_DigestAlg) Type() protoreflect.EnumType {
+ return &file_attest_server_proto_enumTypes[0]
+}
+
+func (x PCR_DigestAlg) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use PCR_DigestAlg.Descriptor instead.
+func (PCR_DigestAlg) EnumDescriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{1, 0}
+}
+
+type Quote struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ TpmVersion uint32 `protobuf:"varint,1,opt,name=tpm_version,json=Version,proto3" json:"tpm_version,omitempty"`
+ Quote []byte `protobuf:"bytes,2,opt,name=quote,json=Quote,proto3" json:"quote,omitempty"`
+ Signature []byte `protobuf:"bytes,3,opt,name=signature,json=Signature,proto3" json:"signature,omitempty"`
+}
+
+func (x *Quote) Reset() {
+ *x = Quote{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Quote) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Quote) ProtoMessage() {}
+
+func (x *Quote) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Quote.ProtoReflect.Descriptor instead.
+func (*Quote) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Quote) GetTpmVersion() uint32 {
+ if x != nil {
+ return x.TpmVersion
+ }
+ return 0
+}
+
+func (x *Quote) GetQuote() []byte {
+ if x != nil {
+ return x.Quote
+ }
+ return nil
+}
+
+func (x *Quote) GetSignature() []byte {
+ if x != nil {
+ return x.Signature
+ }
+ return nil
+}
+
+type PCR struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Index uint32 `protobuf:"varint,1,opt,name=index,json=Index,proto3" json:"index,omitempty"`
+ Digest []byte `protobuf:"bytes,2,opt,name=digest,json=Digest,proto3" json:"digest,omitempty"`
+ DigestAlg PCR_DigestAlg `protobuf:"varint,3,opt,name=digest_alg,json=DigestAlg,proto3,enum=fuhry.runtime.service.attest.PCR_DigestAlg" json:"digest_alg,omitempty"`
+}
+
+func (x *PCR) Reset() {
+ *x = PCR{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PCR) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PCR) ProtoMessage() {}
+
+func (x *PCR) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PCR.ProtoReflect.Descriptor instead.
+func (*PCR) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PCR) GetIndex() uint32 {
+ if x != nil {
+ return x.Index
+ }
+ return 0
+}
+
+func (x *PCR) GetDigest() []byte {
+ if x != nil {
+ return x.Digest
+ }
+ return nil
+}
+
+func (x *PCR) GetDigestAlg() PCR_DigestAlg {
+ if x != nil {
+ return x.DigestAlg
+ }
+ return PCR_INVALID_DIGEST
+}
+
+type AttestationParameters struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Public []byte `protobuf:"bytes,1,opt,name=public,json=Public,proto3" json:"public,omitempty"`
+ UseTcsdActivationFormat bool `protobuf:"varint,2,opt,name=use_tcsd_activation_format,json=UseTCSDActivationFormat,proto3" json:"use_tcsd_activation_format,omitempty"`
+ CreateData []byte `protobuf:"bytes,3,opt,name=create_data,json=CreateData,proto3" json:"create_data,omitempty"`
+ CreateAttestation []byte `protobuf:"bytes,4,opt,name=create_attestation,json=CreateAttestation,proto3" json:"create_attestation,omitempty"`
+ CreateSignature []byte `protobuf:"bytes,5,opt,name=create_signature,json=CreateSignature,proto3" json:"create_signature,omitempty"`
+}
+
+func (x *AttestationParameters) Reset() {
+ *x = AttestationParameters{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AttestationParameters) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttestationParameters) ProtoMessage() {}
+
+func (x *AttestationParameters) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AttestationParameters.ProtoReflect.Descriptor instead.
+func (*AttestationParameters) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *AttestationParameters) GetPublic() []byte {
+ if x != nil {
+ return x.Public
+ }
+ return nil
+}
+
+func (x *AttestationParameters) GetUseTcsdActivationFormat() bool {
+ if x != nil {
+ return x.UseTcsdActivationFormat
+ }
+ return false
+}
+
+func (x *AttestationParameters) GetCreateData() []byte {
+ if x != nil {
+ return x.CreateData
+ }
+ return nil
+}
+
+func (x *AttestationParameters) GetCreateAttestation() []byte {
+ if x != nil {
+ return x.CreateAttestation
+ }
+ return nil
+}
+
+func (x *AttestationParameters) GetCreateSignature() []byte {
+ if x != nil {
+ return x.CreateSignature
+ }
+ return nil
+}
+
+type PlatformParameters struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ TpmVersion uint32 `protobuf:"varint,1,opt,name=tpm_version,json=TPMVersion,proto3" json:"tpm_version,omitempty"`
+ Public []byte `protobuf:"bytes,2,opt,name=public,json=Public,proto3" json:"public,omitempty"`
+ Quotes []*Quote `protobuf:"bytes,3,rep,name=quotes,json=Quotes,proto3" json:"quotes,omitempty"`
+ Pcrs []*PCR `protobuf:"bytes,4,rep,name=pcrs,json=PCRs,proto3" json:"pcrs,omitempty"`
+ EventLog []byte `protobuf:"bytes,5,opt,name=event_log,json=EventLog,proto3" json:"event_log,omitempty"`
+}
+
+func (x *PlatformParameters) Reset() {
+ *x = PlatformParameters{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PlatformParameters) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PlatformParameters) ProtoMessage() {}
+
+func (x *PlatformParameters) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PlatformParameters.ProtoReflect.Descriptor instead.
+func (*PlatformParameters) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *PlatformParameters) GetTpmVersion() uint32 {
+ if x != nil {
+ return x.TpmVersion
+ }
+ return 0
+}
+
+func (x *PlatformParameters) GetPublic() []byte {
+ if x != nil {
+ return x.Public
+ }
+ return nil
+}
+
+func (x *PlatformParameters) GetQuotes() []*Quote {
+ if x != nil {
+ return x.Quotes
+ }
+ return nil
+}
+
+func (x *PlatformParameters) GetPcrs() []*PCR {
+ if x != nil {
+ return x.Pcrs
+ }
+ return nil
+}
+
+func (x *PlatformParameters) GetEventLog() []byte {
+ if x != nil {
+ return x.EventLog
+ }
+ return nil
+}
+
+type GetActivationChallengeRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ TpmVersion uint32 `protobuf:"varint,1,opt,name=tpm_version,json=tpmVersion,proto3" json:"tpm_version,omitempty"`
+ AttestationParameters *AttestationParameters `protobuf:"bytes,2,opt,name=attestation_parameters,json=attestationParameters,proto3" json:"attestation_parameters,omitempty"`
+ EndorsementKey []byte `protobuf:"bytes,3,opt,name=endorsement_key,json=endorsementKey,proto3" json:"endorsement_key,omitempty"`
+}
+
+func (x *GetActivationChallengeRequest) Reset() {
+ *x = GetActivationChallengeRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetActivationChallengeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActivationChallengeRequest) ProtoMessage() {}
+
+func (x *GetActivationChallengeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetActivationChallengeRequest.ProtoReflect.Descriptor instead.
+func (*GetActivationChallengeRequest) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetActivationChallengeRequest) GetTpmVersion() uint32 {
+ if x != nil {
+ return x.TpmVersion
+ }
+ return 0
+}
+
+func (x *GetActivationChallengeRequest) GetAttestationParameters() *AttestationParameters {
+ if x != nil {
+ return x.AttestationParameters
+ }
+ return nil
+}
+
+func (x *GetActivationChallengeRequest) GetEndorsementKey() []byte {
+ if x != nil {
+ return x.EndorsementKey
+ }
+ return nil
+}
+
+type GetActivationChallengeResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ EncryptedSecret []byte `protobuf:"bytes,2,opt,name=encrypted_secret,json=encryptedSecret,proto3" json:"encrypted_secret,omitempty"`
+ Credential []byte `protobuf:"bytes,3,opt,name=credential,proto3" json:"credential,omitempty"`
+}
+
+func (x *GetActivationChallengeResponse) Reset() {
+ *x = GetActivationChallengeResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetActivationChallengeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetActivationChallengeResponse) ProtoMessage() {}
+
+func (x *GetActivationChallengeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetActivationChallengeResponse.ProtoReflect.Descriptor instead.
+func (*GetActivationChallengeResponse) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetActivationChallengeResponse) GetEncryptedSecret() []byte {
+ if x != nil {
+ return x.EncryptedSecret
+ }
+ return nil
+}
+
+func (x *GetActivationChallengeResponse) GetCredential() []byte {
+ if x != nil {
+ return x.Credential
+ }
+ return nil
+}
+
+type AttestPlatformRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Nonce []byte `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"`
+ PlatformParameters *PlatformParameters `protobuf:"bytes,2,opt,name=platform_parameters,json=platformParameters,proto3" json:"platform_parameters,omitempty"`
+}
+
+func (x *AttestPlatformRequest) Reset() {
+ *x = AttestPlatformRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AttestPlatformRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttestPlatformRequest) ProtoMessage() {}
+
+func (x *AttestPlatformRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AttestPlatformRequest.ProtoReflect.Descriptor instead.
+func (*AttestPlatformRequest) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *AttestPlatformRequest) GetNonce() []byte {
+ if x != nil {
+ return x.Nonce
+ }
+ return nil
+}
+
+func (x *AttestPlatformRequest) GetPlatformParameters() *PlatformParameters {
+ if x != nil {
+ return x.PlatformParameters
+ }
+ return nil
+}
+
+type AttestPlatformResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Nonce []byte `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"`
+}
+
+func (x *AttestPlatformResponse) Reset() {
+ *x = AttestPlatformResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_attest_server_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AttestPlatformResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AttestPlatformResponse) ProtoMessage() {}
+
+func (x *AttestPlatformResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_attest_server_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AttestPlatformResponse.ProtoReflect.Descriptor instead.
+func (*AttestPlatformResponse) Descriptor() ([]byte, []int) {
+ return file_attest_server_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *AttestPlatformResponse) GetNonce() []byte {
+ if x != nil {
+ return x.Nonce
+ }
+ return nil
+}
+
+var File_attest_server_proto protoreflect.FileDescriptor
+
+var file_attest_server_proto_rawDesc = []byte{
+ 0x0a, 0x13, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e,
+ 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74,
+ 0x65, 0x73, 0x74, 0x22, 0x59, 0x0a, 0x05, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x1c, 0x0a, 0x0b,
+ 0x74, 0x70, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75,
+ 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x51, 0x75, 0x6f, 0x74, 0x65,
+ 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xc2,
+ 0x01, 0x0a, 0x03, 0x50, 0x43, 0x52, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06,
+ 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x44, 0x69,
+ 0x67, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0a, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x5f, 0x61,
+ 0x6c, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79,
+ 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x43, 0x52, 0x2e, 0x44, 0x69, 0x67, 0x65,
+ 0x73, 0x74, 0x41, 0x6c, 0x67, 0x52, 0x09, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x41, 0x6c, 0x67,
+ 0x22, 0x41, 0x0a, 0x09, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x41, 0x6c, 0x67, 0x12, 0x12, 0x0a,
+ 0x0e, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x44, 0x49, 0x47, 0x45, 0x53, 0x54, 0x10,
+ 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x48, 0x41, 0x31, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53,
+ 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x48, 0x41, 0x33, 0x38,
+ 0x34, 0x10, 0x03, 0x22, 0xe7, 0x01, 0x0a, 0x15, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a,
+ 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x50,
+ 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x3b, 0x0a, 0x1a, 0x75, 0x73, 0x65, 0x5f, 0x74, 0x63, 0x73,
+ 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x6f, 0x72,
+ 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x55, 0x73, 0x65, 0x54, 0x43,
+ 0x53, 0x44, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6f, 0x72, 0x6d,
+ 0x61, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x61, 0x74,
+ 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
+ 0x61, 0x74, 0x61, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x74,
+ 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69,
+ 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x69, 0x67,
+ 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x43, 0x72,
+ 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xde, 0x01,
+ 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
+ 0x74, 0x65, 0x72, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x70, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73,
+ 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x54, 0x50, 0x4d, 0x56, 0x65,
+ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x3b, 0x0a,
+ 0x06, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e,
+ 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x51, 0x75, 0x6f,
+ 0x74, 0x65, 0x52, 0x06, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x04, 0x70, 0x63,
+ 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79,
+ 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x43, 0x52, 0x52, 0x04, 0x50, 0x43, 0x52,
+ 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x05,
+ 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x6f, 0x67, 0x22, 0xd5,
+ 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+ 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x70, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x74, 0x70, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
+ 0x6e, 0x12, 0x6a, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+ 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x33, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d,
+ 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74,
+ 0x2e, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61,
+ 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x27, 0x0a,
+ 0x0f, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d,
+ 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74,
+ 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x6e, 0x63, 0x72,
+ 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63,
+ 0x72, 0x65, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
+ 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
+ 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x90, 0x01, 0x0a, 0x15, 0x41, 0x74,
+ 0x74, 0x65, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x61, 0x0a, 0x13, 0x70, 0x6c, 0x61,
+ 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72,
+ 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61,
+ 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x50, 0x61,
+ 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, 0x12, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f,
+ 0x72, 0x6d, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x2e, 0x0a, 0x16,
+ 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x32, 0x9f, 0x02, 0x0a,
+ 0x06, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x12, 0x95, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41,
+ 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e,
+ 0x67, 0x65, 0x12, 0x3b, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69,
+ 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73,
+ 0x74, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
+ 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x3c, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e,
+ 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x47,
+ 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x6c,
+ 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
+ 0x7d, 0x0a, 0x0e, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72,
+ 0x6d, 0x12, 0x33, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d,
+ 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74,
+ 0x2e, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72,
+ 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x61,
+ 0x74, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x50, 0x6c, 0x61, 0x74,
+ 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2b,
+ 0x5a, 0x29, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x72,
+ 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72,
+ 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_attest_server_proto_rawDescOnce sync.Once
+ file_attest_server_proto_rawDescData = file_attest_server_proto_rawDesc
+)
+
+func file_attest_server_proto_rawDescGZIP() []byte {
+ file_attest_server_proto_rawDescOnce.Do(func() {
+ file_attest_server_proto_rawDescData = protoimpl.X.CompressGZIP(file_attest_server_proto_rawDescData)
+ })
+ return file_attest_server_proto_rawDescData
+}
+
+var file_attest_server_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_attest_server_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_attest_server_proto_goTypes = []interface{}{
+ (PCR_DigestAlg)(0), // 0: fuhry.runtime.service.attest.PCR.DigestAlg
+ (*Quote)(nil), // 1: fuhry.runtime.service.attest.Quote
+ (*PCR)(nil), // 2: fuhry.runtime.service.attest.PCR
+ (*AttestationParameters)(nil), // 3: fuhry.runtime.service.attest.AttestationParameters
+ (*PlatformParameters)(nil), // 4: fuhry.runtime.service.attest.PlatformParameters
+ (*GetActivationChallengeRequest)(nil), // 5: fuhry.runtime.service.attest.GetActivationChallengeRequest
+ (*GetActivationChallengeResponse)(nil), // 6: fuhry.runtime.service.attest.GetActivationChallengeResponse
+ (*AttestPlatformRequest)(nil), // 7: fuhry.runtime.service.attest.AttestPlatformRequest
+ (*AttestPlatformResponse)(nil), // 8: fuhry.runtime.service.attest.AttestPlatformResponse
+}
+var file_attest_server_proto_depIdxs = []int32{
+ 0, // 0: fuhry.runtime.service.attest.PCR.digest_alg:type_name -> fuhry.runtime.service.attest.PCR.DigestAlg
+ 1, // 1: fuhry.runtime.service.attest.PlatformParameters.quotes:type_name -> fuhry.runtime.service.attest.Quote
+ 2, // 2: fuhry.runtime.service.attest.PlatformParameters.pcrs:type_name -> fuhry.runtime.service.attest.PCR
+ 3, // 3: fuhry.runtime.service.attest.GetActivationChallengeRequest.attestation_parameters:type_name -> fuhry.runtime.service.attest.AttestationParameters
+ 4, // 4: fuhry.runtime.service.attest.AttestPlatformRequest.platform_parameters:type_name -> fuhry.runtime.service.attest.PlatformParameters
+ 5, // 5: fuhry.runtime.service.attest.Attest.GetActivationChallenge:input_type -> fuhry.runtime.service.attest.GetActivationChallengeRequest
+ 7, // 6: fuhry.runtime.service.attest.Attest.AttestPlatform:input_type -> fuhry.runtime.service.attest.AttestPlatformRequest
+ 6, // 7: fuhry.runtime.service.attest.Attest.GetActivationChallenge:output_type -> fuhry.runtime.service.attest.GetActivationChallengeResponse
+ 8, // 8: fuhry.runtime.service.attest.Attest.AttestPlatform:output_type -> fuhry.runtime.service.attest.AttestPlatformResponse
+ 7, // [7:9] is the sub-list for method output_type
+ 5, // [5:7] is the sub-list for method input_type
+ 5, // [5:5] is the sub-list for extension type_name
+ 5, // [5:5] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_attest_server_proto_init() }
+func file_attest_server_proto_init() {
+ if File_attest_server_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_attest_server_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Quote); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PCR); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AttestationParameters); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PlatformParameters); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetActivationChallengeRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetActivationChallengeResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AttestPlatformRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_attest_server_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AttestPlatformResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_attest_server_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 8,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_attest_server_proto_goTypes,
+ DependencyIndexes: file_attest_server_proto_depIdxs,
+ EnumInfos: file_attest_server_proto_enumTypes,
+ MessageInfos: file_attest_server_proto_msgTypes,
+ }.Build()
+ File_attest_server_proto = out.File
+ file_attest_server_proto_rawDesc = nil
+ file_attest_server_proto_goTypes = nil
+ file_attest_server_proto_depIdxs = nil
+}
--- /dev/null
+syntax = "proto3";
+
+option go_package = "go.fuhry.dev/runtime/proto/service/attest";
+
+package fuhry.runtime.service.attest;
+
+service Attest {
+ rpc GetActivationChallenge (GetActivationChallengeRequest) returns (GetActivationChallengeResponse) {}
+ rpc AttestPlatform (AttestPlatformRequest) returns (AttestPlatformResponse) {}
+}
+
+message Quote {
+ uint32 tpm_version = 1 [json_name = "Version"];
+ bytes quote = 2 [json_name = "Quote"];
+ bytes signature = 3 [json_name = "Signature"];
+}
+
+message PCR {
+ enum DigestAlg {
+ INVALID_DIGEST = 0;
+ SHA1 = 1;
+ SHA256 = 2;
+ SHA384 = 3;
+ }
+
+ uint32 index = 1 [json_name = "Index"];
+ bytes digest = 2 [json_name = "Digest"];
+ DigestAlg digest_alg = 3 [json_name = "DigestAlg"];
+}
+
+message AttestationParameters {
+ bytes public = 1 [json_name = "Public"];
+ bool use_tcsd_activation_format = 2 [json_name = "UseTCSDActivationFormat"];
+ bytes create_data = 3 [json_name = "CreateData"];
+ bytes create_attestation = 4 [json_name = "CreateAttestation"];
+ bytes create_signature = 5 [json_name = "CreateSignature"];
+}
+
+message PlatformParameters {
+ uint32 tpm_version = 1 [json_name = "TPMVersion"];
+ bytes public = 2 [json_name = "Public"];
+ repeated Quote quotes = 3 [json_name = "Quotes"];
+ repeated PCR pcrs = 4 [json_name = "PCRs"];
+ bytes event_log = 5 [json_name = "EventLog"];
+}
+
+message GetActivationChallengeRequest {
+ uint32 tpm_version = 1;
+ AttestationParameters attestation_parameters = 2;
+ bytes endorsement_key = 3;
+}
+
+message GetActivationChallengeResponse {
+ reserved 1;
+
+ bytes encrypted_secret = 2;
+ bytes credential = 3;
+}
+
+message AttestPlatformRequest {
+ bytes nonce = 1;
+ PlatformParameters platform_parameters = 2;
+}
+
+message AttestPlatformResponse {
+ bytes nonce = 1;
+}
--- /dev/null
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v4.25.1
+// source: attest_server.proto
+
+package attest
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ Attest_GetActivationChallenge_FullMethodName = "/fuhry.runtime.service.attest.Attest/GetActivationChallenge"
+ Attest_AttestPlatform_FullMethodName = "/fuhry.runtime.service.attest.Attest/AttestPlatform"
+)
+
+// AttestClient is the client API for Attest service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type AttestClient interface {
+ GetActivationChallenge(ctx context.Context, in *GetActivationChallengeRequest, opts ...grpc.CallOption) (*GetActivationChallengeResponse, error)
+ AttestPlatform(ctx context.Context, in *AttestPlatformRequest, opts ...grpc.CallOption) (*AttestPlatformResponse, error)
+}
+
+type attestClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewAttestClient(cc grpc.ClientConnInterface) AttestClient {
+ return &attestClient{cc}
+}
+
+func (c *attestClient) GetActivationChallenge(ctx context.Context, in *GetActivationChallengeRequest, opts ...grpc.CallOption) (*GetActivationChallengeResponse, error) {
+ out := new(GetActivationChallengeResponse)
+ err := c.cc.Invoke(ctx, Attest_GetActivationChallenge_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *attestClient) AttestPlatform(ctx context.Context, in *AttestPlatformRequest, opts ...grpc.CallOption) (*AttestPlatformResponse, error) {
+ out := new(AttestPlatformResponse)
+ err := c.cc.Invoke(ctx, Attest_AttestPlatform_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// AttestServer is the server API for Attest service.
+// All implementations must embed UnimplementedAttestServer
+// for forward compatibility
+type AttestServer interface {
+ GetActivationChallenge(context.Context, *GetActivationChallengeRequest) (*GetActivationChallengeResponse, error)
+ AttestPlatform(context.Context, *AttestPlatformRequest) (*AttestPlatformResponse, error)
+ mustEmbedUnimplementedAttestServer()
+}
+
+// UnimplementedAttestServer must be embedded to have forward compatible implementations.
+type UnimplementedAttestServer struct {
+}
+
+func (UnimplementedAttestServer) GetActivationChallenge(context.Context, *GetActivationChallengeRequest) (*GetActivationChallengeResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetActivationChallenge not implemented")
+}
+func (UnimplementedAttestServer) AttestPlatform(context.Context, *AttestPlatformRequest) (*AttestPlatformResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AttestPlatform not implemented")
+}
+func (UnimplementedAttestServer) mustEmbedUnimplementedAttestServer() {}
+
+// UnsafeAttestServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to AttestServer will
+// result in compilation errors.
+type UnsafeAttestServer interface {
+ mustEmbedUnimplementedAttestServer()
+}
+
+func RegisterAttestServer(s grpc.ServiceRegistrar, srv AttestServer) {
+ s.RegisterService(&Attest_ServiceDesc, srv)
+}
+
+func _Attest_GetActivationChallenge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetActivationChallengeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AttestServer).GetActivationChallenge(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Attest_GetActivationChallenge_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AttestServer).GetActivationChallenge(ctx, req.(*GetActivationChallengeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Attest_AttestPlatform_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AttestPlatformRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AttestServer).AttestPlatform(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Attest_AttestPlatform_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AttestServer).AttestPlatform(ctx, req.(*AttestPlatformRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Attest_ServiceDesc is the grpc.ServiceDesc for Attest service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Attest_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "fuhry.runtime.service.attest.Attest",
+ HandlerType: (*AttestServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "GetActivationChallenge",
+ Handler: _Attest_GetActivationChallenge_Handler,
+ },
+ {
+ MethodName: "AttestPlatform",
+ Handler: _Attest_AttestPlatform_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "attest_server.proto",
+}
--- /dev/null
+package attest
+
+import (
+ "crypto"
+ "fmt"
+
+ "github.com/google/go-attestation/attest"
+)
+
+func AttestationParametersFromProto(pb *AttestationParameters) attest.AttestationParameters {
+ return attest.AttestationParameters{
+ Public: pb.Public,
+ UseTCSDActivationFormat: pb.UseTcsdActivationFormat,
+ CreateData: pb.CreateData,
+ CreateAttestation: pb.CreateAttestation,
+ CreateSignature: pb.CreateSignature,
+ }
+}
+
+func AttestationParametersToProto(ap attest.AttestationParameters) *AttestationParameters {
+ return &AttestationParameters{
+ Public: ap.Public,
+ UseTcsdActivationFormat: ap.UseTCSDActivationFormat,
+ CreateData: ap.CreateData,
+ CreateAttestation: ap.CreateAttestation,
+ CreateSignature: ap.CreateSignature,
+ }
+}
+
+func QuoteFromProto(pb *Quote) attest.Quote {
+ return attest.Quote{
+ Version: attest.TPMVersion(pb.TpmVersion),
+ Quote: pb.Quote,
+ Signature: pb.Signature,
+ }
+}
+
+func QuoteToProto(qu attest.Quote) *Quote {
+ return &Quote{
+ TpmVersion: uint32(qu.Version),
+ Quote: qu.Quote,
+ Signature: qu.Signature,
+ }
+}
+
+func PCRFromProto(pb *PCR) (*attest.PCR, error) {
+ var alg crypto.Hash
+
+ switch pb.DigestAlg {
+ case PCR_SHA1:
+ alg = crypto.SHA1
+ case PCR_SHA256:
+ alg = crypto.SHA256
+ case PCR_SHA384:
+ alg = crypto.SHA384
+ default:
+ return nil, fmt.Errorf("Unsupported digest alg: %d", pb.DigestAlg)
+ }
+
+ return &attest.PCR{
+ Index: int(pb.Index),
+ Digest: pb.Digest,
+ DigestAlg: alg,
+ }, nil
+}
+
+func PCRToProto(ap *attest.PCR) (*PCR, error) {
+ var alg PCR_DigestAlg
+
+ digest := ap.DigestAlg.String()
+ switch digest {
+ case "SHA-1":
+ alg = PCR_SHA1
+ case "SHA-256":
+ alg = PCR_SHA256
+ case "SHA-384":
+ alg = PCR_SHA384
+ default:
+ return nil, fmt.Errorf("Unsupported PCR digest algorithm: %s", digest)
+ }
+
+ return &PCR{
+ Index: uint32(ap.Index),
+ Digest: ap.Digest,
+ DigestAlg: alg,
+ }, nil
+}
+
+func PlatformParametersFromProto(pb *PlatformParameters) (*attest.PlatformParameters, error) {
+ pp := &attest.PlatformParameters{
+ TPMVersion: attest.TPMVersion(pb.TpmVersion),
+ Public: pb.Public,
+ EventLog: pb.EventLog,
+ Quotes: make([]attest.Quote, len(pb.Quotes)),
+ PCRs: make([]attest.PCR, len(pb.Pcrs)),
+ }
+
+ for i, q := range pb.Quotes {
+ pp.Quotes[i] = QuoteFromProto(q)
+ }
+
+ for i, p := range pb.Pcrs {
+ pcr, err := PCRFromProto(p)
+ if err != nil {
+ return nil, err
+ }
+ pp.PCRs[i] = *pcr
+ }
+
+ return pp, nil
+}
+
+func PlatformParametersToProto(pp *attest.PlatformParameters) (*PlatformParameters, error) {
+ pb := &PlatformParameters{
+ TpmVersion: uint32(pp.TPMVersion),
+ Public: pp.Public,
+ EventLog: pp.EventLog,
+ Quotes: make([]*Quote, len(pp.Quotes)),
+ Pcrs: make([]*PCR, len(pp.PCRs)),
+ }
+
+ for i, q := range pp.Quotes {
+ pb.Quotes[i] = QuoteToProto(q)
+ }
+
+ for i, p := range pp.PCRs {
+ pcr, err := PCRToProto(&p)
+ if err != nil {
+ return nil, err
+ }
+ pb.Pcrs[i] = pcr
+ }
+
+ return pb, nil
+}
+
+func (r *GetActivationChallengeResponse) EC() attest.EncryptedCredential {
+ return attest.EncryptedCredential{
+ Credential: r.Credential,
+ Secret: r.EncryptedSecret,
+ }
+}
--- /dev/null
+PROTO_SRCS := $(wildcard *.proto)
+PROTO_GO_OUTPUT := $(PROTO_SRCS:.proto=.pb.go)
+
+$(PROTO_GO_OUTPUT): $(PROTO_SRCS)
+ protoc --go_out=. --go_opt=paths=source_relative \
+ --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+ $(PROTO_SRCS)
+
+pb_go: $(PROTO_GO_OUTPUT)
+
+all: pb_go
\ No newline at end of file
--- /dev/null
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.30.0
+// protoc v4.25.1
+// source: echo_server.proto
+
+package echo
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type EchoRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *EchoRequest) Reset() {
+ *x = EchoRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_echo_server_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *EchoRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EchoRequest) ProtoMessage() {}
+
+func (x *EchoRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_echo_server_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead.
+func (*EchoRequest) Descriptor() ([]byte, []int) {
+ return file_echo_server_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *EchoRequest) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+type EchoReply struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *EchoReply) Reset() {
+ *x = EchoReply{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_echo_server_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *EchoReply) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EchoReply) ProtoMessage() {}
+
+func (x *EchoReply) ProtoReflect() protoreflect.Message {
+ mi := &file_echo_server_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use EchoReply.ProtoReflect.Descriptor instead.
+func (*EchoReply) Descriptor() ([]byte, []int) {
+ return file_echo_server_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *EchoReply) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+type GreetRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+}
+
+func (x *GreetRequest) Reset() {
+ *x = GreetRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_echo_server_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GreetRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GreetRequest) ProtoMessage() {}
+
+func (x *GreetRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_echo_server_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GreetRequest.ProtoReflect.Descriptor instead.
+func (*GreetRequest) Descriptor() ([]byte, []int) {
+ return file_echo_server_proto_rawDescGZIP(), []int{2}
+}
+
+type GreetReply struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+}
+
+func (x *GreetReply) Reset() {
+ *x = GreetReply{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_echo_server_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GreetReply) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GreetReply) ProtoMessage() {}
+
+func (x *GreetReply) ProtoReflect() protoreflect.Message {
+ mi := &file_echo_server_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GreetReply.ProtoReflect.Descriptor instead.
+func (*GreetReply) Descriptor() ([]byte, []int) {
+ return file_echo_server_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GreetReply) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+var File_echo_server_proto protoreflect.FileDescriptor
+
+var file_echo_server_proto_rawDesc = []byte{
+ 0x0a, 0x11, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x12, 0x1a, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69,
+ 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x22,
+ 0x27, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18,
+ 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x25, 0x0a, 0x09, 0x45, 0x63, 0x68, 0x6f,
+ 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22,
+ 0x0e, 0x0a, 0x0c, 0x47, 0x72, 0x65, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
+ 0x26, 0x0a, 0x0a, 0x47, 0x72, 0x65, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a,
+ 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
+ 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xbd, 0x01, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f,
+ 0x12, 0x58, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x27, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79,
+ 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+ 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x25, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d,
+ 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45,
+ 0x63, 0x68, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x05, 0x47, 0x72,
+ 0x65, 0x65, 0x74, 0x12, 0x28, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74,
+ 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f,
+ 0x2e, 0x47, 0x72, 0x65, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e,
+ 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x47, 0x72, 0x65, 0x65, 0x74,
+ 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x6f, 0x2e, 0x66, 0x75,
+ 0x68, 0x72, 0x79, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x65, 0x63,
+ 0x68, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_echo_server_proto_rawDescOnce sync.Once
+ file_echo_server_proto_rawDescData = file_echo_server_proto_rawDesc
+)
+
+func file_echo_server_proto_rawDescGZIP() []byte {
+ file_echo_server_proto_rawDescOnce.Do(func() {
+ file_echo_server_proto_rawDescData = protoimpl.X.CompressGZIP(file_echo_server_proto_rawDescData)
+ })
+ return file_echo_server_proto_rawDescData
+}
+
+var file_echo_server_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_echo_server_proto_goTypes = []interface{}{
+ (*EchoRequest)(nil), // 0: fuhry.runtime.service.echo.EchoRequest
+ (*EchoReply)(nil), // 1: fuhry.runtime.service.echo.EchoReply
+ (*GreetRequest)(nil), // 2: fuhry.runtime.service.echo.GreetRequest
+ (*GreetReply)(nil), // 3: fuhry.runtime.service.echo.GreetReply
+}
+var file_echo_server_proto_depIdxs = []int32{
+ 0, // 0: fuhry.runtime.service.echo.Echo.Echo:input_type -> fuhry.runtime.service.echo.EchoRequest
+ 2, // 1: fuhry.runtime.service.echo.Echo.Greet:input_type -> fuhry.runtime.service.echo.GreetRequest
+ 1, // 2: fuhry.runtime.service.echo.Echo.Echo:output_type -> fuhry.runtime.service.echo.EchoReply
+ 3, // 3: fuhry.runtime.service.echo.Echo.Greet:output_type -> fuhry.runtime.service.echo.GreetReply
+ 2, // [2:4] is the sub-list for method output_type
+ 0, // [0:2] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_echo_server_proto_init() }
+func file_echo_server_proto_init() {
+ if File_echo_server_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_echo_server_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*EchoRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_echo_server_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*EchoReply); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_echo_server_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GreetRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_echo_server_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GreetReply); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_echo_server_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_echo_server_proto_goTypes,
+ DependencyIndexes: file_echo_server_proto_depIdxs,
+ MessageInfos: file_echo_server_proto_msgTypes,
+ }.Build()
+ File_echo_server_proto = out.File
+ file_echo_server_proto_rawDesc = nil
+ file_echo_server_proto_goTypes = nil
+ file_echo_server_proto_depIdxs = nil
+}
--- /dev/null
+syntax = "proto3";
+
+option go_package = "go.fuhry.dev/runtime/proto/service/echo";
+
+package fuhry.runtime.service.echo;
+
+service Echo {
+ rpc Echo(EchoRequest) returns (EchoReply) {}
+ rpc Greet(GreetRequest) returns (GreetReply) {}
+}
+
+message EchoRequest {
+ string message = 1;
+}
+
+message EchoReply {
+ string message = 1;
+}
+
+message GreetRequest {}
+
+message GreetReply {
+ string message = 1;
+}
--- /dev/null
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v4.25.1
+// source: echo_server.proto
+
+package echo
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+ Echo_Echo_FullMethodName = "/fuhry.runtime.service.echo.Echo/Echo"
+ Echo_Greet_FullMethodName = "/fuhry.runtime.service.echo.Echo/Greet"
+)
+
+// EchoClient is the client API for Echo service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type EchoClient interface {
+ Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoReply, error)
+ Greet(ctx context.Context, in *GreetRequest, opts ...grpc.CallOption) (*GreetReply, error)
+}
+
+type echoClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewEchoClient(cc grpc.ClientConnInterface) EchoClient {
+ return &echoClient{cc}
+}
+
+func (c *echoClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoReply, error) {
+ out := new(EchoReply)
+ err := c.cc.Invoke(ctx, Echo_Echo_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *echoClient) Greet(ctx context.Context, in *GreetRequest, opts ...grpc.CallOption) (*GreetReply, error) {
+ out := new(GreetReply)
+ err := c.cc.Invoke(ctx, Echo_Greet_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// EchoServer is the server API for Echo service.
+// All implementations must embed UnimplementedEchoServer
+// for forward compatibility
+type EchoServer interface {
+ Echo(context.Context, *EchoRequest) (*EchoReply, error)
+ Greet(context.Context, *GreetRequest) (*GreetReply, error)
+ mustEmbedUnimplementedEchoServer()
+}
+
+// UnimplementedEchoServer must be embedded to have forward compatible implementations.
+type UnimplementedEchoServer struct {
+}
+
+func (UnimplementedEchoServer) Echo(context.Context, *EchoRequest) (*EchoReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented")
+}
+func (UnimplementedEchoServer) Greet(context.Context, *GreetRequest) (*GreetReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Greet not implemented")
+}
+func (UnimplementedEchoServer) mustEmbedUnimplementedEchoServer() {}
+
+// UnsafeEchoServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to EchoServer will
+// result in compilation errors.
+type UnsafeEchoServer interface {
+ mustEmbedUnimplementedEchoServer()
+}
+
+func RegisterEchoServer(s grpc.ServiceRegistrar, srv EchoServer) {
+ s.RegisterService(&Echo_ServiceDesc, srv)
+}
+
+func _Echo_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(EchoRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(EchoServer).Echo(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Echo_Echo_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(EchoServer).Echo(ctx, req.(*EchoRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Echo_Greet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GreetRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(EchoServer).Greet(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Echo_Greet_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(EchoServer).Greet(ctx, req.(*GreetRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Echo_ServiceDesc is the grpc.ServiceDesc for Echo service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Echo_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "fuhry.runtime.service.echo.Echo",
+ HandlerType: (*EchoServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Echo",
+ Handler: _Echo_Echo_Handler,
+ },
+ {
+ MethodName: "Greet",
+ Handler: _Echo_Greet_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "echo_server.proto",
+}
--- /dev/null
+package rand
+
+import (
+ "crypto/rand"
+ "math/big"
+)
+
+func Range(low int64, high int64) int64 {
+ max := big.NewInt(high - low)
+ n, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ panic(err)
+ }
+ return n.Int64() + low
+}
--- /dev/null
+package sase
+
+import (
+ "net"
+)
+
+type ACL interface {
+ Test(req *WebSocketRequest) bool
+ AddRule(...ACLRule)
+
+ Rules() []ACLRule
+ DefaultAction() ACLAction
+}
+
+type acl struct {
+ rules []ACLRule
+ defaultAction ACLAction
+}
+
+type ACLRule interface {
+ Test(req *WebSocketRequest) ACLAction
+}
+
+type aclMatcher interface {
+ matchesRequest(req *WebSocketRequest) bool
+}
+
+type ACLRuleType uint
+type ACLAction uint
+
+const (
+ ActionContinue ACLAction = iota
+ ActionDeny
+ ActionAllow
+)
+
+const (
+ RuleTypeIPNetwork ACLRuleType = iota
+ RuleTypeHostnamePattern
+ RuleTypeHostnameRegexp
+)
+
+type aclRule struct {
+ ACLRule
+
+ match []aclMatcher
+ action ACLAction
+}
+
+type aclIPNetworkRule struct {
+ network *net.IPNet
+}
+
+func NewACL(defaultAction ACLAction) *acl {
+ acl := &acl{
+ rules: make([]ACLRule, 0),
+ defaultAction: defaultAction,
+ }
+
+ return acl
+}
+
+func (acl *acl) AddRule(rule ...ACLRule) {
+ acl.rules = append(acl.rules, rule...)
+}
+
+func (acl *acl) Rules() []ACLRule {
+ return acl.rules
+}
+
+func (acl *acl) DefaultAction() ACLAction {
+ return acl.defaultAction
+}
+
+func (acl *acl) Test(req *WebSocketRequest) bool {
+ for _, rule := range acl.rules {
+ action := rule.Test(req)
+ if action != ActionContinue {
+ return action == ActionAllow
+ }
+ }
+
+ return acl.defaultAction == ActionAllow
+}
+
+func NewACLRule(action ACLAction, match ...aclMatcher) ACLRule {
+ return &aclRule{
+ action: action,
+ match: match,
+ }
+}
+
+func (rule *aclRule) Test(req *WebSocketRequest) ACLAction {
+ matches := true
+evalLoop:
+ for _, matcher := range rule.match {
+ if !matcher.matchesRequest(req) {
+ matches = false
+ // lazy evaluation
+ break evalLoop
+ }
+ }
+
+ if matches {
+ return rule.action
+ }
+
+ return ActionContinue
+}
+
+func NewIPNetworkMatcher(net *net.IPNet) aclMatcher {
+ return &aclIPNetworkRule{
+ network: net,
+ }
+}
+
+func (r *aclIPNetworkRule) matchesRequest(req *WebSocketRequest) bool {
+ for _, ip := range req.addr {
+ if r.network.Contains(net.ParseIP(ip.String())) {
+ return true
+ }
+ }
+
+ return false
+}
--- /dev/null
+package sase
+
+import (
+ "net"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ACLSuite struct{}
+
+var _ = Suite(&ACLSuite{})
+
+func (s *ACLSuite) TestIPNetworkRule(c *C) {
+ type testCase struct {
+ expect bool
+ req WebSocketRequest
+ }
+
+ cases := []testCase{
+ {
+ expect: true,
+ req: WebSocketRequest{
+ network: "tcp",
+ hostname: "localhost",
+ port: 22,
+ addr: []net.Addr{
+ &net.IPAddr{IP: net.ParseIP("127.0.0.1")},
+ &net.IPAddr{IP: net.ParseIP("::1")},
+ },
+ },
+ },
+ {
+ expect: false,
+ req: WebSocketRequest{
+ network: "tcp",
+ hostname: "localhost",
+ port: 22,
+ addr: []net.Addr{
+ &net.IPAddr{IP: net.ParseIP("10.0.0.1")},
+ &net.IPAddr{IP: net.ParseIP("fe80::1")},
+ },
+ },
+ },
+ }
+
+ acl := NewACL(ActionDeny)
+
+ _, cidr4, _ := net.ParseCIDR("127.0.0.0/8")
+ _, cidr6, _ := net.ParseCIDR("::1/128")
+ acl.AddRule(NewACLRule(ActionAllow, NewIPNetworkMatcher(cidr4)))
+ acl.AddRule(NewACLRule(ActionAllow, NewIPNetworkMatcher(cidr6)))
+
+ for _, testCase := range cases {
+ c.Assert(acl.Test(&testCase.req), Equals, testCase.expect)
+ }
+}
--- /dev/null
+package sase
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+
+ "github.com/gorilla/websocket"
+ "go.fuhry.dev/runtime/mtls"
+)
+
+type Client interface {
+ Connect(*url.URL) (io.ReadWriteCloser, error)
+}
+
+type saseTcpProxyClient struct {
+ Client
+
+ wsClient *websocket.Dialer
+ identity mtls.Identity
+}
+
+func NewClient(id mtls.Identity) (Client, error) {
+ tlsConfig, err := id.TlsConfig(context.TODO())
+ if err != nil {
+ return nil, err
+ }
+ tlsConfig.RootCAs = nil
+ wsClient := &websocket.Dialer{
+ TLSClientConfig: tlsConfig,
+ }
+
+ return &saseTcpProxyClient{
+ identity: id,
+ wsClient: wsClient,
+ }, nil
+}
+
+func (c *saseTcpProxyClient) Connect(url *url.URL) (io.ReadWriteCloser, error) {
+ conn, resp, err := c.wsClient.Dial(url.String(), nil)
+ if err != nil {
+ if resp != nil {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("received unexpected response from server: HTTP %d: %s", resp.StatusCode, body)
+ }
+ return nil, err
+ }
+
+ return newWebSocketBinaryReadWriter(conn)
+}
--- /dev/null
+package sase
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "strings"
+ "sync"
+)
+
+func dialHappyEyeballs(ctx context.Context, addrs []net.Addr, port uint16) (net.Conn, error) {
+ dialer := &net.Dialer{}
+ lock := &sync.Mutex{}
+ defer lock.Unlock()
+ errors := make([]error, 0)
+ connChan := make(chan net.Conn, len(addrs))
+ defer close(connChan)
+ errChan := make(chan error, len(addrs))
+ defer close(errChan)
+ done := false
+ defer (func() { done = true })()
+ defer lock.Lock()
+ dialContext, dialCancel := context.WithCancel(ctx)
+
+ dialFunc := func(addr net.Addr) {
+ addr = &net.TCPAddr{
+ IP: net.ParseIP(addr.String()),
+ Port: int(port),
+ }
+ conn, err := dialer.DialContext(dialContext, addr.Network(), addr.String())
+ lock.Lock()
+ defer lock.Unlock()
+ if err != nil {
+ if !strings.HasSuffix(err.Error(), ": operation was canceled") && !done {
+ errChan <- err
+ }
+ return
+ }
+ if !done {
+ connChan <- conn
+ }
+ }
+
+ for _, addr := range addrs {
+ go dialFunc(addr)
+ }
+
+ for {
+ select {
+ case conn := <-connChan:
+ dialCancel()
+ return conn, nil
+ case err := <-errChan:
+ errors = append(errors, err)
+ if len(errors) == len(addrs) {
+ dialCancel()
+ return nil, fmt.Errorf("failed to make any connection: %v", errors)
+ }
+ case <-ctx.Done():
+ dialCancel()
+ return nil, context.Canceled
+ }
+ }
+}
--- /dev/null
+package sase
+
+import (
+ "flag"
+ "fmt"
+ "net"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/machines"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type machinesDomain struct {
+ domainName string
+ cidr4 *net.IPNet
+ cidr6 *net.IPNet
+}
+
+type machinesApi_DomainEntry struct {
+ Name string `json:"name"`
+ IPv4NetworkAddress string `json:"inet4_address"`
+ IPv4PrefixLength int `json:"inet4_prefixlen"`
+ IPv6NetworkAddress string `json:"inet6_address"`
+ IPv6PrefixLength int `json:"inet6_prefixlen"`
+}
+
+type machinesApi_DomainResponse []*machinesApi_DomainEntry
+
+var defaultDomains []machinesDomain
+
+func init() {
+ type _domain struct {
+ domainName string
+ cidr4 string
+ cidr6 string
+ }
+
+ unparsedDomains := []_domain{
+ {constants.DefaultHostDomain, "10.0.0.0/22", "fe00:ab00::/64"},
+ }
+
+ defaultDomains = make([]machinesDomain, 0)
+ for _, d := range unparsedDomains {
+ domain := machinesDomain{
+ domainName: d.domainName,
+ cidr4: mustParseIPCidr(d.cidr4),
+ cidr6: mustParseIPCidr(d.cidr6),
+ }
+
+ defaultDomains = append(defaultDomains, domain)
+ }
+}
+
+func initNetworksFromMachinesApi(client machines.MachinesClient) {
+ domains := make(machinesApi_DomainResponse, 0)
+ err := client.APICall("domains", nil, &domains)
+ if err != nil {
+ log.Default().Errorf("failed to get domains from the machines api: %v\n", err)
+ return
+ }
+ out := make([]machinesDomain, 0)
+ for _, domain := range domains {
+ xd := machinesDomain{
+ domainName: domain.Name,
+ cidr4: mustParseIPCidr(fmt.Sprintf("%s/%d", domain.IPv4NetworkAddress, domain.IPv4PrefixLength)),
+ cidr6: mustParseIPCidr(fmt.Sprintf("%s/%d", domain.IPv6NetworkAddress, domain.IPv6PrefixLength)),
+ }
+ out = append(out, xd)
+ }
+
+ if len(out) > 0 {
+ defaultDomains = out
+ }
+}
+
+func NewCorpNetworkRuleset() []ACLRule {
+ if !flag.Parsed() {
+ log.Fatal("args are not yet parsed, so we cannot load domains")
+ }
+
+ client, err := machines.NewDefaultMachinesClient("domain.list")
+ if err == nil {
+ initNetworksFromMachinesApi(client)
+ }
+ rules := make([]ACLRule, 0)
+ for _, d := range defaultDomains {
+ rules = append(rules, NewACLRule(ActionAllow, NewIPNetworkMatcher(d.cidr4)))
+ rules = append(rules, NewACLRule(ActionAllow, NewIPNetworkMatcher(d.cidr6)))
+ }
+ return rules
+}
+
+func mustParseIPCidr(cidr string) *net.IPNet {
+ _, ipnet, err := net.ParseCIDR(cidr)
+ if err != nil {
+ panic(err)
+ }
+
+ return ipnet
+}
--- /dev/null
+package sase
+
+import (
+ "context"
+ "io"
+
+ "github.com/gorilla/websocket"
+ "go.fuhry.dev/runtime/utils/generics"
+)
+
+type webSocketBinaryReadWriter struct {
+ io.ReadWriteCloser
+
+ ws *websocket.Conn
+ curMsg []byte
+}
+
+func newWebSocketBinaryReadWriter(ws *websocket.Conn) (*webSocketBinaryReadWriter, error) {
+ return &webSocketBinaryReadWriter{
+ ws: ws,
+ }, nil
+}
+
+func (rw *webSocketBinaryReadWriter) Read(p []byte) (int, error) {
+ if rw.curMsg == nil {
+ t, msg, err := rw.ws.ReadMessage()
+ if err != nil {
+ return 0, err
+ }
+
+ if t == websocket.BinaryMessage {
+ rw.curMsg = msg
+ }
+ }
+
+ if rw.curMsg != nil {
+ n := generics.Min(len(p), len(rw.curMsg))
+ copy(p, rw.curMsg)
+ if n < len(rw.curMsg) {
+ rw.curMsg = rw.curMsg[n:]
+ } else {
+ rw.curMsg = nil
+ }
+
+ return n, nil
+ }
+
+ return 0, nil
+}
+
+func (rw *webSocketBinaryReadWriter) Write(p []byte) (int, error) {
+ err := rw.ws.WriteMessage(websocket.BinaryMessage, p)
+ return len(p), err
+}
+
+func (rw *webSocketBinaryReadWriter) Close() error {
+ return rw.ws.Close()
+}
+
+func NewChanReader(reader io.Reader, cancel context.CancelFunc) chan []byte {
+ readChan := make(chan []byte)
+
+ go (func() {
+ defer close(readChan)
+ defer cancel()
+
+ buf := make([]byte, 64*1024)
+ for {
+ n, err := reader.Read(buf)
+ if n > 0 && (err == nil || err == io.EOF) {
+ replyBuf := make([]byte, n)
+ copy(replyBuf, buf)
+ readChan <- replyBuf
+ if err == io.EOF {
+ return
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+ })()
+
+ return readChan
+}
--- /dev/null
+package sase
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "regexp"
+ "strconv"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "go.fuhry.dev/runtime/net/dns"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type WebSocketProxy struct {
+ Server *http.Server
+ ACL ACL
+}
+
+type WebSocketRequest struct {
+ network string
+ hostname string
+ port uint16
+ addr []net.Addr
+}
+
+type logEntry struct {
+ RemoteAddress,
+ Host,
+ Method,
+ Path string
+
+ StatusCode int
+}
+
+type statusRecorder struct {
+ http.ResponseWriter
+ http.Hijacker
+
+ Status int
+}
+
+var (
+ pathRegexp *regexp.Regexp
+)
+
+var upgrader = &websocket.Upgrader{
+ ReadBufferSize: 1280,
+ WriteBufferSize: 1280,
+}
+
+func init() {
+ pathRegexp = regexp.MustCompile(`^/(?P<network>[a-z]{1,8})/(?P<hostname>[a-z0-9-]+(?:\.[a-z0-9-]+)*)/(?P<port>[0-9]{1,5})$`)
+}
+
+func NewWebSocketProxy(listen string) (*WebSocketProxy, error) {
+ handler := http.NewServeMux()
+ server := &http.Server{
+ Addr: listen,
+ Handler: newLoggingMiddleware(handler),
+ }
+
+ wsp := &WebSocketProxy{
+ Server: server,
+ ACL: NewACL(ActionDeny),
+ }
+
+ handler.HandleFunc("/tcp/", wsp.handle)
+ handler.HandleFunc("/_/health", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("content-type", "text/plain")
+ w.WriteHeader(200)
+ fmt.Fprintf(w, "OK\n")
+ })
+
+ return wsp, nil
+}
+
+func (wsp *WebSocketProxy) handle(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+
+ if !pathRegexp.MatchString(path) {
+ wsp.replyForbidden(w, "invalid path: does not match expected request pattern")
+ return
+ }
+
+ groups := pathRegexp.FindAllStringSubmatch(path, 1)
+ senames := pathRegexp.SubexpNames()
+
+ wsReq := &WebSocketRequest{
+ addr: make([]net.Addr, 0, 2),
+ }
+
+ for _, matches := range groups {
+ for i, match := range matches {
+ switch senames[i] {
+ case "network":
+ wsReq.network = match
+ case "hostname":
+ wsReq.hostname = match
+ case "port":
+ matchInt, err := strconv.Atoi(match)
+ if err != nil {
+ wsp.replyBadRequest(w, "invalid port: cannot parse as integer")
+ return
+ }
+ wsReq.port = uint16(matchInt)
+ }
+ }
+ }
+
+ ipv4, ipv6, err := dns.ResolveDualStack(wsReq.hostname)
+ if err != nil {
+ wsp.replyBadGateway(w, fmt.Sprintf("cannot resolve hostname %q: %v", wsReq.hostname, err))
+ return
+ }
+
+ if ipv4 != "" {
+ wsReq.addr = append(wsReq.addr, &net.IPAddr{IP: net.ParseIP(ipv4)})
+ }
+ if ipv6 != "" {
+ wsReq.addr = append(wsReq.addr, &net.IPAddr{IP: net.ParseIP(ipv6)})
+ }
+
+ if !wsp.ACL.Test(wsReq) {
+ wsp.replyForbidden(w, "ACL denied access to this hostname/IP/port")
+ return
+ }
+
+ reqCtx := r.Context()
+ dialCtx, dialCancel := context.WithTimeout(reqCtx, 5*time.Second)
+ defer dialCancel()
+
+ conn, err := dialHappyEyeballs(dialCtx, wsReq.addr, wsReq.port)
+
+ if err != nil {
+ wsp.replyBadGateway(w, err.Error())
+ return
+ }
+
+ defer conn.Close()
+
+ ws, err := upgrader.Upgrade(w, r, w.Header())
+ if err != nil {
+ wsp.replyServerError(w, err.Error())
+ return
+ }
+
+ wsReadWriter, err := newWebSocketBinaryReadWriter(ws)
+ if err != nil {
+ wsp.replyServerError(w, err.Error())
+ return
+ }
+
+ wsCtx, cancel := context.WithCancel(reqCtx)
+
+ localReader := NewChanReader(wsReadWriter, cancel)
+ remoteReader := NewChanReader(conn, cancel)
+
+mainLoop:
+ for {
+ select {
+ case msg := <-localReader:
+ conn.Write(msg)
+ case msg := <-remoteReader:
+ wsReadWriter.Write(msg)
+ case <-wsCtx.Done():
+ break mainLoop
+ }
+ }
+
+ cancel()
+}
+
+func (wsp *WebSocketProxy) replyBadGateway(w http.ResponseWriter, msg string) {
+ w.Header().Set("content-type", "text/plain")
+ w.WriteHeader(502)
+ fmt.Fprintf(w, "502 Bad Gateway: %s\n", msg)
+}
+
+func (wsp *WebSocketProxy) replyServerError(w http.ResponseWriter, msg string) {
+ w.Header().Set("content-type", "text/plain")
+ w.WriteHeader(500)
+ fmt.Fprintf(w, "500 Internal Server Error: %s\n", msg)
+}
+
+func (wsp *WebSocketProxy) replyForbidden(w http.ResponseWriter, msg string) {
+ w.Header().Set("content-type", "text/plain")
+ w.WriteHeader(403)
+ fmt.Fprintf(w, "403 Forbidden: %s\n", msg)
+}
+
+func (wsp *WebSocketProxy) replyBadRequest(w http.ResponseWriter, msg string) {
+ w.Header().Set("content-type", "text/plain")
+ w.WriteHeader(400)
+ fmt.Fprintf(w, "400 Bad Request: %s\n", msg)
+}
+
+func newLoggingMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ws := &statusRecorder{
+ ResponseWriter: w,
+ }
+ if h, ok := w.(http.Hijacker); ok {
+ ws.Hijacker = h
+ }
+
+ h.ServeHTTP(ws, r)
+
+ logger := log.Default()
+
+ entry := logEntry{
+ RemoteAddress: r.RemoteAddr,
+ Host: r.Host,
+ Method: r.Method,
+ Path: r.URL.Path,
+ StatusCode: ws.Status,
+ }
+
+ entryJson, err := json.Marshal(entry)
+ if err == nil {
+ logger.Print(string(entryJson))
+ }
+ })
+}
+
+func (r *statusRecorder) WriteHeader(status int) {
+ r.ResponseWriter.WriteHeader(status)
+ r.Status = status
+}
+
+func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ r.Status = http.StatusSwitchingProtocols
+ return r.Hijacker.Hijack()
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "net/url"
+ "os"
+ "os/signal"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/sase"
+)
+
+func main() {
+ mtls.SetDefaultIdentity("devicetrust")
+
+ flag.Parse()
+
+ url, err := url.Parse(flag.Arg(0))
+
+ if err != nil {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ if url.Scheme != "wss" {
+ fmt.Fprintln(os.Stderr, "only wss is supported as a url scheme")
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ id := mtls.DefaultIdentity()
+
+ client, err := sase.NewClient(id)
+ if err != nil {
+ panic(err)
+ }
+
+ conn, err := client.Connect(url)
+ if err != nil {
+ panic(err)
+ }
+
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
+
+ remoteChan := sase.NewChanReader(conn, cancel)
+ localChan := sase.NewChanReader(os.Stdin, cancel)
+
+mainLoop:
+ for {
+ select {
+ case msg := <-remoteChan:
+ os.Stdout.Write(msg)
+ case msg := <-localChan:
+ conn.Write(msg)
+ case <-ctx.Done():
+ break mainLoop
+ }
+ }
+
+ cancel()
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os/signal"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/rand"
+ "go.fuhry.dev/runtime/sase"
+ "go.fuhry.dev/runtime/sd"
+)
+
+func main() {
+ var addr string
+ var sslCert string
+
+ defaultPort := int(rand.Range(1025, 59999))
+
+ flag.StringVar(&addr, "addr", fmt.Sprintf("[::]:%d", defaultPort), "address to listen on")
+ flag.StringVar(&sslCert, "cert", "", "puppet-managed ssl cert to use; if left blank, TLS is disabled")
+
+ flag.Parse()
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ listenPort, err := strconv.Atoi(addr[strings.LastIndex(addr, ":")+1:])
+ if err != nil {
+ panic(err)
+ }
+
+ wsp, err := sase.NewWebSocketProxy(addr)
+ if err != nil {
+ panic(err)
+ }
+
+ wsp.ACL.AddRule(sase.NewCorpNetworkRuleset()...)
+
+ if sslCert != "" {
+ cert := mtls.NewSSLCertificate(sslCert)
+ if !cert.IsValid() {
+ panic("ssl certificate is not usable")
+ }
+
+ tlsc, err := cert.TlsConfig(ctx)
+ if err != nil {
+ panic(err)
+ }
+
+ verifier := mtls.NewPeerNameVerifier()
+ verifier.AllowFrom(mtls.Domain, "."+constants.RootDomain)
+ verifier.AllowFrom(mtls.Service, "devicetrust")
+ err = verifier.ConfigureServer(tlsc)
+ if err != nil {
+ panic(err)
+ }
+
+ wsp.Server.TLSConfig = tlsc
+ go wsp.Server.ListenAndServeTLS("", "")
+ } else {
+ go wsp.Server.ListenAndServe()
+ }
+
+ publisher := &sd.SDPublisher{
+ Protocol: sd.ProtocolTCP,
+ Service: "sase-proxy",
+ AdvertisePort: uint16(listenPort),
+ }
+
+ err = publisher.Publish(ctx)
+ if err != nil {
+ panic(err)
+ }
+
+ <-ctx.Done()
+
+ publisher.Unpublish()
+
+ tmo, tmoCancel := context.WithTimeout(context.Background(), 5*time.Second)
+
+ wsp.Server.Shutdown(tmo)
+ tmoCancel()
+}
--- /dev/null
+package sd
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "sync"
+ "time"
+
+ "go.etcd.io/etcd/client/pkg/v3/srv"
+ etcd_client "go.etcd.io/etcd/client/v3"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+var clientSingleton *etcd_client.Client
+var clientMtx sync.Mutex
+var etcdMtlsId = "etcd-client"
+var etcdDiscoveryDomain string
+var etcdStartupTimeoutMs uint = 15000
+
+func NewDefaultEtcdClient() (*etcd_client.Client, error) {
+ var err error
+
+ clientMtx.Lock()
+ defer clientMtx.Unlock()
+
+ logger := log.WithPrefix("etcd-client")
+
+ id := mtls.NewServiceIdentity(etcdMtlsId)
+
+ if clientSingleton == nil {
+ deadline := time.Now().Add(time.Millisecond * time.Duration(etcdStartupTimeoutMs))
+ for {
+ if time.Now().After(deadline) && err != nil {
+ return nil, err
+ }
+
+ clientSingleton, err = NewEtcdClient(id)
+ if err == nil {
+ break
+ }
+
+ logger.Warnf("failed to startup etcd client: %+v", err)
+ logger.Warn("waiting 1sec before retrying")
+
+ time.Sleep(1 * time.Second)
+ }
+ }
+
+ return clientSingleton, nil
+}
+
+func NewEtcdClient(id mtls.Identity) (*etcd_client.Client, error) {
+ deadline := time.Now().Add(time.Millisecond * time.Duration(etcdStartupTimeoutMs))
+
+ return NewEtcdClientWithDeadline(
+ id, deadline)
+}
+
+func NewEtcdClientWithDeadline(id mtls.Identity, deadline time.Time) (*etcd_client.Client, error) {
+ var client *etcd_client.Client
+
+ tlsConfig, err := id.TlsConfig(context.Background())
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup client TLS configuration: %v", err)
+ }
+ pnv := mtls.NewPeerNameVerifier()
+ pnv.AllowFrom(mtls.Service, "etcd")
+ pnv.ConfigureClient(tlsConfig)
+
+ clients, err := srv.GetClient("etcd-client", etcdDiscoveryDomain, "")
+ if err != nil {
+ return nil, fmt.Errorf("failed to discover peers: %v", err)
+ }
+
+ dialTimeout := time.Until(deadline)
+
+ clientConfig := etcd_client.Config{
+ DialTimeout: dialTimeout,
+ Endpoints: clients.Endpoints,
+ TLS: tlsConfig,
+ }
+
+ // clients.Endpoints is the list of endpoints now
+ client, err = etcd_client.New(clientConfig)
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to create client: %v", err)
+ }
+
+ return client, nil
+}
+
+func init() {
+ flag.StringVar(&etcdMtlsId, "etcd.mtls.id", "etcd-client", "mTLS identity to use for connecting to etcd")
+ flag.UintVar(&etcdStartupTimeoutMs, "etcd.startup-timeout", etcdStartupTimeoutMs, "max timeout (in ms) for etcd startup attempts before failing")
+ flag.StringVar(&etcdDiscoveryDomain, "etcd.srv-domain", hostname.DomainName(), "discovery domain for etcd")
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ "os/signal"
+ "sync"
+ "syscall"
+
+ etcd_client "go.etcd.io/etcd/client/v3"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/sd"
+)
+
+func main() {
+ var listen string
+ flag.StringVar(&listen, "listen", ":7080", "IP address and port to listen on")
+
+ flag.Parse()
+
+ client, err := sd.NewDefaultEtcdClient()
+ if err != nil {
+ panic(err)
+ }
+ defer client.Close()
+ keys := make(map[string][]byte, 0)
+ keysLock := &sync.RWMutex{}
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ handler := http.NewServeMux()
+ server := &http.Server{
+ Handler: handler,
+ }
+
+ handler.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
+ keysLock.RLock()
+ defer keysLock.RUnlock()
+
+ w.Header().Set("content-type", "text/plain; version=0.0.4; charset=utf-8")
+
+ gaugeName := "sd_service_health"
+ fmt.Fprintf(w, "# HELP %s Health of services as monitored by the sdregister helper\n", gaugeName)
+ fmt.Fprintf(w, "# TYPE %s gauge\n", gaugeName)
+
+ for _, serviceJson := range keys {
+ s := sd.SDHealthReport{}
+
+ err := json.Unmarshal(serviceJson, &s)
+ if err != nil {
+ continue
+ }
+
+ metric := 0.0
+ if s.Up {
+ metric = 1.0
+ }
+ labels := fmt.Sprintf(
+ "protocol=%q,service=%q,monitor_host=%q,monitor_region=%q",
+ s.Protocol, s.Service, s.MonitorHost, s.MonitorRegion)
+
+ if s.Shard != "" {
+ labels += fmt.Sprintf(",shard_name=%q", s.Shard)
+ }
+ if s.ShardRegion != "" {
+ labels += fmt.Sprintf(",shard_region=%q", s.ShardRegion)
+ }
+
+ fmt.Fprintf(w, "%s{%s} %.1f\n", gaugeName, labels, metric)
+ }
+ })
+
+ tcpAddr, err := net.ResolveTCPAddr("tcp", listen)
+ if err != nil {
+ panic(err)
+ }
+ tlsListener, err := makeTlsListener(tcpAddr, ctx)
+ if err != nil {
+ panic(err)
+ }
+ go server.Serve(tlsListener)
+
+ publisher := &sd.SDPublisher{
+ AdvertisePort: tcpAddr.AddrPort().Port(),
+ Protocol: sd.ProtocolTCP,
+ Service: "health-exporter",
+ }
+ publisher.Publish(ctx)
+
+ initialKeys, err := client.Get(ctx, "/sd/_health/", etcd_client.WithPrefix())
+ if err != nil {
+ panic(err)
+ }
+ keysLock.Lock()
+ for _, entry := range initialKeys.Kvs {
+ keys[string(entry.Key)] = entry.Value
+ }
+ keysLock.Unlock()
+
+ watcher := client.Watch(ctx, "/sd/_health/", etcd_client.WithPrefix())
+ for {
+ select {
+ case notif := <-watcher:
+ keysLock.Lock()
+ for _, event := range notif.Events {
+ switch event.Type.String() {
+ case "PUT":
+ keys[string(event.Kv.Key)] = event.Kv.Value
+ case "DELETE":
+ k := string(event.Kv.Key)
+ delete(keys, k)
+ }
+ }
+ keysLock.Unlock()
+ case <-ctx.Done():
+ publisher.Unpublish()
+ server.Shutdown(context.Background())
+ return
+ }
+ }
+}
+
+func makeTlsListener(tcpAddr *net.TCPAddr, ctx context.Context) (net.Listener, error) {
+ mtlsId := mtls.DefaultIdentity()
+
+ netListener, err := net.ListenTCP("tcp", tcpAddr)
+ if err != nil {
+ return nil, err
+ }
+ tlsc, err := mtlsId.TlsConfig(ctx)
+ if err != nil {
+ return nil, err
+ }
+ cv := mtls.NewPeerNameVerifier()
+ err = cv.ConfigureServer(tlsc)
+ if err != nil {
+ return nil, err
+ }
+ cv.AllowFrom(mtls.Service, "prometheus")
+ cv.AllowFrom(mtls.User, "dan")
+
+ return tls.NewListener(netListener, tlsc), nil
+}
--- /dev/null
+package sd
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type HealthCheckEngine interface {
+ Name() string
+ Load(serviceDef *ServiceDefinition, config []byte) (HealthCheckService, error)
+}
+
+type HealthCheckService interface {
+ Check() error
+ ServiceDefinition() *ServiceDefinition
+ NewPublisher(domain string, regions []string) *SDPublisher
+ NewDefaultPublisher() *SDPublisher
+}
+
+type PublishAddress struct {
+ IPOrHostname string
+ Port uint16
+}
+
+type Layer4Protocol uint8
+
+const (
+ ProtocolInvalid Layer4Protocol = iota
+ ProtocolTCP
+ ProtocolUDP
+ ProtocolGRPC
+)
+
+var (
+ ErrTLSServerOptionConflict = errors.New(
+ "mTLS related options (\"peer_mtls_id\") are mutually exclusive with \"server_name\" " +
+ "and \"ca_certificate\"")
+ ErrTLSClientOptionConflict = errors.New(
+ "cannot set client cert or private key when mtls identities are used")
+)
+
+type HealthStatus uint
+
+type ServiceDefinition struct {
+ Engine string `json:"type"`
+ ServiceName string `json:"service_name"`
+ PollInterval uint `json:"poll_interval"`
+}
+
+type HealthChangeEvent struct {
+ Status HealthStatus
+ Error error
+}
+
+const (
+ ServiceUp HealthStatus = iota
+ ServiceDown
+)
+
+type socketService struct {
+ SocketHost string `json:"socket_host"`
+ SocketPort uint16 `json:"socket_port"`
+}
+
+type tlsService struct {
+ UseTls bool `json:"tls"`
+ MtlsIdentity string `json:"mtls_id"`
+ MtlsPeer string `json:"peer_mtls_id"`
+ ServerName string `json:"server_name"`
+ ClientCert string `json:"certificate"`
+ PrivateKey string `json:"private_key"`
+ CaCert string `json:"ca_certificate"`
+}
+
+var engines []HealthCheckEngine
+var enginesOnce sync.Once
+
+func (p Layer4Protocol) DNSComponent() string {
+ switch p {
+ case ProtocolTCP:
+ return "_tcp"
+ case ProtocolUDP:
+ return "_udp"
+ case ProtocolGRPC:
+ return "_grpc"
+ default:
+ panic("cannot stringify invalid protocol")
+ }
+}
+
+func (p Layer4Protocol) String() string {
+ return p.DNSComponent()[1:]
+}
+
+func Layer4ProtocolFromString(s string) (Layer4Protocol, error) {
+ switch s {
+ case "udp", "UDP", ProtocolUDP.DNSComponent():
+ return ProtocolUDP, nil
+ case "tcp", "TCP", ProtocolTCP.DNSComponent():
+ return ProtocolTCP, nil
+ case "grpc", "GRPC", "gRPC", ProtocolGRPC.DNSComponent():
+ return ProtocolGRPC, nil
+ default:
+ return ProtocolInvalid, fmt.Errorf("%q is not a valid layer4 protocol id", s)
+ }
+}
+
+func LoadHealthCheck(conf []byte) (HealthCheckService, error) {
+ svc := &ServiceDefinition{}
+ err := json.Unmarshal(conf, svc)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, engine := range engines {
+ if svc.Engine == engine.Name() {
+ service, err := engine.Load(svc, conf)
+ if err != nil {
+ return nil, err
+ }
+ return service, nil
+ }
+ }
+
+ return nil, fmt.Errorf("service type %s is unsupported", svc.Engine)
+}
+
+func HealthCheckLoop(ctx context.Context, svc HealthCheckService) <-chan HealthChangeEvent {
+ logger := log.WithPrefix("HealthCheckLoop")
+ ticker := time.NewTicker(time.Second * time.Duration(svc.ServiceDefinition().PollInterval))
+ eventsChan := make(chan HealthChangeEvent)
+
+ go (func() {
+ var lastEvent *HealthChangeEvent
+
+ doHealthCheck := func() {
+ var event HealthChangeEvent
+ err := svc.Check()
+ if err == nil {
+ logger.V(1).Infof("%T: svc %s is healthy", svc, svc.ServiceDefinition().ServiceName)
+ event = HealthChangeEvent{
+ Status: ServiceUp,
+ Error: nil,
+ }
+ } else {
+ logger.V(1).Infof("%T: svc %s is unhealthy: %v", svc, svc.ServiceDefinition().ServiceName, err)
+ event = HealthChangeEvent{
+ Status: ServiceDown,
+ Error: err,
+ }
+ }
+ if lastEvent == nil || lastEvent.Status != event.Status {
+ eventsChan <- event
+ }
+ lastEvent = &event
+ }
+
+ doHealthCheck()
+
+ for {
+ select {
+ case <-ctx.Done():
+ close(eventsChan)
+ return
+ case <-ticker.C:
+ doHealthCheck()
+ }
+ }
+ })()
+
+ return eventsChan
+}
+
+func registerEngine(hce HealthCheckEngine) {
+ enginesOnce.Do(func() {
+ engines = make([]HealthCheckEngine, 0)
+ })
+
+ engines = append(engines, hce)
+}
+
+func newTlsConfigForHealthcheckService(ts *tlsService, ss *socketService) (*tls.Config, error) {
+ logger := log.Default().WithPrefix("healthcheck.tls")
+
+ if !ts.UseTls {
+ return nil, nil
+ }
+
+ var c *tls.Config
+ var err error
+
+ if ts.MtlsIdentity != "" {
+ id := mtls.NewServiceIdentity(ts.MtlsIdentity)
+ if !id.IsValid() {
+ return nil, fmt.Errorf("unable to load mTLS identity %q, check that the certificate and private key are present, up-to-date and accessible", ts.MtlsIdentity)
+ }
+
+ c, err = id.TlsConfig(context.TODO())
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ c = &tls.Config{}
+ }
+
+ if ts.MtlsPeer != "" {
+ if ts.CaCert != "" || ts.ServerName != "" {
+ return nil, ErrTLSServerOptionConflict
+ }
+
+ pnv := mtls.NewPeerNameVerifier()
+ pnv.AllowFrom(mtls.Service, ts.MtlsPeer)
+ err = pnv.ConfigureClient(c)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if ts.CaCert != "" {
+ caCertificates, err := certutil.LoadCertificatesFromPEM(ts.CaCert)
+ if err != nil {
+ return nil, err
+ }
+ if c.RootCAs == nil {
+ c.RootCAs = x509.NewCertPool()
+ }
+ for _, cert := range caCertificates {
+ logger.V(2).Debugf("permit cert: %q", cert.Subject.String())
+ c.RootCAs.AddCert(cert)
+ }
+ }
+
+ if ts.ClientCert != "" && ts.PrivateKey != "" {
+ if c.GetClientCertificate != nil {
+ return nil, ErrTLSClientOptionConflict
+ }
+ clientCertificate, err := certutil.LoadCertificatesFromPEM(ts.ClientCert)
+ if err != nil {
+ return nil, err
+ }
+ clientPrivateKey, err := certutil.LoadPrivateKeyFromPEM(ts.PrivateKey)
+ if err != nil {
+ return nil, err
+ }
+ if len(clientCertificate) < 1 {
+ return nil, fmt.Errorf("no certificates found in client certificate path %s", ts.ClientCert)
+ }
+ rawCerts := make([][]byte, 0)
+ for _, c := range clientCertificate {
+ rawCerts = append(rawCerts, c.Raw)
+ }
+
+ tlsCerts := []tls.Certificate{
+ {
+ Certificate: rawCerts,
+ PrivateKey: clientPrivateKey,
+ Leaf: clientCertificate[0],
+ },
+ }
+
+ c.Certificates = tlsCerts
+ }
+ if ts.ServerName != "" {
+ c.ServerName = ts.ServerName
+ } else if ss.SocketHost != "" {
+ c.ServerName = ss.SocketHost
+ }
+
+ logger.V(3).Debugf("finished assembling tls.Config: %+v", c)
+ return c, nil
+}
--- /dev/null
+package sd
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/hostname"
+)
+
+type httpHealthCheckEngine struct {
+ HealthCheckEngine
+}
+
+type httpHealthCheckService struct {
+ HealthCheckService
+
+ socketService
+ tlsService
+
+ serviceDef *ServiceDefinition `json:"-"`
+ httpClient *http.Client `json:"-"`
+
+ HttpHost string `json:"http_host"`
+ Http2 bool `json:"http2"`
+ Path string `json:"path"`
+}
+
+func (hce *httpHealthCheckEngine) Name() string {
+ return "http"
+}
+
+func (hce *httpHealthCheckEngine) Load(serviceDef *ServiceDefinition, conf []byte) (HealthCheckService, error) {
+ svc := &httpHealthCheckService{
+ serviceDef: serviceDef,
+ }
+
+ err := json.Unmarshal(conf, svc)
+ if err != nil {
+ return nil, err
+ }
+
+ tlsConfig, err := newTlsConfigForHealthcheckService(&svc.tlsService, &svc.socketService)
+ if err != nil {
+ return nil, err
+ }
+
+ svc.httpClient = &http.Client{
+ Transport: &http.Transport{
+ ForceAttemptHTTP2: svc.Http2,
+ TLSClientConfig: tlsConfig,
+ },
+ }
+
+ return svc, nil
+}
+
+func (hcs *httpHealthCheckService) ServiceDefinition() *ServiceDefinition {
+ return hcs.serviceDef
+}
+
+func (hcs *httpHealthCheckService) NewPublisher(domain string, regions []string) *SDPublisher {
+ _, port := hcs.PublishAddress()
+
+ publisher := &SDPublisher{
+ AdvertiseHost: hcs.SocketHost,
+ AdvertisePort: port,
+ Domain: domain,
+ Protocol: ProtocolTCP,
+ Service: hcs.serviceDef.ServiceName,
+ ShardName: strings.Split(hcs.SocketHost, ".")[0],
+ Regions: regions,
+ }
+
+ return publisher
+}
+
+func (hcs *httpHealthCheckService) NewDefaultPublisher() *SDPublisher {
+ domain := constants.SDDomain
+ regions := []string{
+ hostname.RegionName(),
+ }
+
+ return hcs.NewPublisher(domain, regions)
+}
+
+func (hcs *httpHealthCheckService) PublishAddress() (string, uint16) {
+ port := hcs.SocketPort
+ if port == 0 {
+ port = 80
+ if hcs.UseTls {
+ port = 443
+ }
+ }
+
+ return hcs.SocketHost, port
+}
+
+func (hcs *httpHealthCheckService) Check() error {
+ protocol := "http"
+ var port uint16 = 80
+ if hcs.UseTls {
+ protocol = "https"
+ port = 443
+ }
+ if hcs.SocketPort != 0 {
+ port = hcs.SocketPort
+ }
+ path := hcs.Path
+ if path == "" {
+ path = "/"
+ }
+ if path[0] != '/' {
+ path = "/" + path
+ }
+ url := fmt.Sprintf("%s://%s:%d%s", protocol, hcs.SocketHost, port, path)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+
+ if hcs.HttpHost != "" {
+ req.Host = hcs.HttpHost
+ } else if hcs.ServerName != "" && hcs.UseTls {
+ req.Host = hcs.ServerName
+ }
+ req.Header.Set("connection", "close")
+
+ resp, err := hcs.httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode >= 200 && resp.StatusCode < 400 {
+ return nil
+ }
+
+ return fmt.Errorf("http request succeeded, but received unhealthy status code: %d", resp.StatusCode)
+}
+
+func init() {
+ hce := &httpHealthCheckEngine{}
+
+ registerEngine(hce)
+}
--- /dev/null
+package sd
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ ldap "github.com/go-ldap/ldap/v3"
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type ldapHealthCheckEngine struct {
+ HealthCheckEngine
+}
+
+type ldapHealthCheckService struct {
+ HealthCheckService
+
+ socketService
+ tlsService
+
+ serviceDef *ServiceDefinition `json:"-"`
+ tlsConfig *tls.Config `json:"-"`
+
+ Query string `json:"search_query"`
+}
+
+func (lce *ldapHealthCheckEngine) Name() string {
+ return "ldap"
+}
+
+func (lce *ldapHealthCheckEngine) Load(serviceDef *ServiceDefinition, conf []byte) (HealthCheckService, error) {
+ svc := &ldapHealthCheckService{
+ serviceDef: serviceDef,
+ }
+
+ err := json.Unmarshal(conf, svc)
+ if err != nil {
+ return nil, err
+ }
+
+ tlsConfig, err := newTlsConfigForHealthcheckService(&svc.tlsService, &svc.socketService)
+ if err != nil {
+ return nil, err
+ }
+
+ svc.tlsConfig = tlsConfig
+
+ return svc, nil
+}
+
+func (lcs *ldapHealthCheckService) ServiceDefinition() *ServiceDefinition {
+ return lcs.serviceDef
+}
+
+func (lcs *ldapHealthCheckService) NewPublisher(domain string, regions []string) *SDPublisher {
+ _, port := lcs.PublishAddress()
+
+ publisher := &SDPublisher{
+ AdvertiseHost: lcs.SocketHost,
+ AdvertisePort: port,
+ Domain: domain,
+ Protocol: ProtocolTCP,
+ Service: lcs.serviceDef.ServiceName,
+ ShardName: strings.Split(lcs.SocketHost, ".")[0],
+ Regions: regions,
+ }
+
+ return publisher
+}
+
+func (lcs *ldapHealthCheckService) NewDefaultPublisher() *SDPublisher {
+ domain := constants.SDDomain
+ region := []string{
+ hostname.RegionName(),
+ }
+
+ return lcs.NewPublisher(domain, region)
+}
+
+func (lcs *ldapHealthCheckService) PublishAddress() (string, uint16) {
+ port := lcs.SocketPort
+ if port == 0 {
+ port = 389
+ if lcs.UseTls {
+ port = 636
+ }
+ }
+
+ return lcs.SocketHost, port
+}
+
+func (lcs *ldapHealthCheckService) Check() error {
+ var err error
+ var port uint16 = 389
+ logger := log.WithPrefix("ldapHealthCheckService:" + lcs.SocketHost)
+
+ if lcs.UseTls {
+ port = 636
+ }
+ if lcs.SocketPort != 0 {
+ port = lcs.SocketPort
+ }
+
+ addr := fmt.Sprintf("%s:%d", lcs.SocketHost, port)
+ var conn *ldap.Conn
+
+ if lcs.UseTls {
+ conn, err = ldap.DialTLS("tcp", addr, lcs.tlsConfig)
+ if err != nil {
+ return err
+ }
+ } else {
+ conn, err = ldap.Dial("tcp", addr)
+ if err != nil {
+ return err
+ }
+ }
+ defer conn.Close()
+
+ err = conn.ExternalBind()
+ if err != nil {
+ return err
+ }
+
+ whoami, err := conn.WhoAmI([]ldap.Control{})
+ if err != nil {
+ return err
+ }
+
+ logger.V(2).Infof("Successfully bound to LDAP as %s", whoami.AuthzID)
+
+ return nil
+}
+
+func init() {
+ lce := &ldapHealthCheckEngine{}
+
+ registerEngine(lce)
+}
--- /dev/null
+package sd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+
+ etcd_client "go.etcd.io/etcd/client/v3"
+)
+
+type healthCheckingPublisher struct {
+ publisher *SDPublisher
+ healthcheck HealthCheckService
+ etcdClient *etcd_client.Client
+ logger *log.Logger
+}
+
+type SDHealthReport struct {
+ Up bool
+ Domain string
+ Shard string
+ ShardRegion string
+ Protocol string
+ Service string
+ SocketHost string
+ SocketPort uint16
+ LastError string
+ MonitorHost string
+ MonitorRegion string
+}
+
+type srvRecord struct {
+ Host string `json:"host"`
+ Priority uint `json:"priority"`
+ Port uint16 `json:"port"`
+ TTL uint `json:"ttl"`
+}
+
+type addressRecord struct {
+ Host string `json:"host"`
+ TTL uint `json:"ttl"`
+}
+
+func (sdp *healthCheckingPublisher) MonitorAndPublish(ctx context.Context, wg *sync.WaitGroup) error {
+ logger := sdp.logger.AppendPrefix(".MonitorAndPublish")
+ ticker := time.NewTicker(30 * time.Second)
+
+ wg.Add(1)
+ defer wg.Done()
+
+ var err error
+
+ leaseApi := etcd_client.NewLease(sdp.etcdClient)
+ statLease, err := leaseApi.Grant(ctx, 60)
+ if err != nil {
+ return err
+ }
+ logger.V(2).Debugf("leaseID: %016x", statLease.ID)
+
+ defer (func() {
+ if statLease != nil {
+ leaseApi.Revoke(context.Background(), statLease.ID)
+ }
+ })()
+
+ events := HealthCheckLoop(ctx, sdp.healthcheck)
+
+ var lastEvent *HealthChangeEvent
+
+ for {
+ select {
+ case evt := <-events:
+ lastEvent = &evt
+ sdp.updateHealth(ctx, statLease, evt)
+ switch evt.Status {
+ case ServiceUp:
+ sdp.publisher.Publish(ctx)
+ case ServiceDown:
+ sdp.publisher.Unpublish()
+ }
+ case <-ticker.C:
+ kar, err := leaseApi.KeepAliveOnce(ctx, statLease.ID)
+ if err != nil || (kar != nil && kar.ID == etcd_client.NoLease) {
+ // our lease got revoked
+ logger.V(1).Errorf("Our lease disappeared!")
+ statLease, err = leaseApi.Grant(ctx, 60)
+ if err != nil {
+ return err
+ }
+ logger.V(2).Debugf("New leaseID: %016x", statLease.ID)
+ if lastEvent != nil {
+ sdp.updateHealth(ctx, statLease, *lastEvent)
+ }
+ }
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+func (sdp *healthCheckingPublisher) updateHealth(ctx context.Context, lease *etcd_client.LeaseGrantResponse, evt HealthChangeEvent) {
+ key := fmt.Sprintf("/sd/_health/%016x", lease.ID)
+
+ hr := SDHealthReport{
+ Up: evt.Status == ServiceUp,
+ Service: sdp.publisher.Service,
+ SocketHost: sdp.publisher.AdvertiseHost,
+ SocketPort: sdp.publisher.AdvertisePort,
+ Domain: sdp.publisher.Domain,
+ Shard: sdp.publisher.ShardName,
+ ShardRegion: strings.Split(sdp.publisher.AdvertiseHost, ".")[1],
+ Protocol: sdp.healthcheck.ServiceDefinition().Engine,
+ MonitorHost: hostname.Fqdn(),
+ MonitorRegion: hostname.RegionName(),
+ }
+ if evt.Error != nil {
+ hr.LastError = evt.Error.Error()
+ }
+ j, err := json.Marshal(hr)
+ if err != nil {
+ return
+ }
+
+ sdp.etcdClient.Put(ctx, key, string(j), etcd_client.WithLease(lease.ID))
+}
+
+func NewHealthCheckPublisher(hcs HealthCheckService, domain string, regions []string) (*healthCheckingPublisher, error) {
+ etcd, err := NewDefaultEtcdClient()
+ if err != nil {
+ return nil, err
+ }
+
+ return &healthCheckingPublisher{
+ publisher: hcs.NewPublisher(domain, regions),
+ healthcheck: hcs,
+ etcdClient: etcd,
+ logger: log.WithPrefix(fmt.Sprintf("HealthCheckingPublisher(%s)", hcs.ServiceDefinition().ServiceName)),
+ }, nil
+}
--- /dev/null
+package sd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ etcd_client "go.etcd.io/etcd/client/v3"
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/net/dns"
+ "go.fuhry.dev/runtime/utils"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+const (
+ leaseRenewalInterval = 15
+ leaseMaxLifetime = 30
+)
+
+type SDPublisher struct {
+ AdvertiseHost string
+ AdvertisePort uint16
+ Domain string
+ Protocol Layer4Protocol
+ Service string
+ ShardName string
+ LocalRegion string
+ Regions []string
+ IP4 string
+ IP6 string
+ EtcdClient *etcd_client.Client
+
+ mtx *sync.Mutex
+ wg *sync.WaitGroup
+
+ logger *log.Logger
+ leases etcd_client.Lease
+ lease *etcd_client.LeaseGrantResponse
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+type recordToPublish struct {
+ rtype string
+ path string
+ val string
+}
+
+func (s *SDPublisher) init() error {
+ if s.mtx == nil {
+ s.mtx = &sync.Mutex{}
+ s.wg = &sync.WaitGroup{}
+ }
+
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ s.logger = log.WithPrefix(fmt.Sprintf("SDPublish(%s)", s.Service))
+
+ if s.EtcdClient == nil {
+ cl, err := NewDefaultEtcdClient()
+
+ if err != nil {
+ return err
+ }
+
+ s.EtcdClient = cl
+ }
+
+ s.leases = etcd_client.NewLease(s.EtcdClient)
+
+ errors := make([]string, 0)
+
+ if s.AdvertiseHost == "" {
+ s.AdvertiseHost = hostname.Fqdn()
+ }
+
+ if s.AdvertisePort == 0 {
+ errors = append(errors, "cannot advertise port 0")
+ }
+
+ if s.Service == "" {
+ errors = append(errors, "service name was not set")
+ }
+
+ if s.ShardName == "" {
+ s.ShardName = hostname.Hostname()
+ }
+
+ if s.LocalRegion == "" {
+ s.LocalRegion = hostname.RegionName()
+ }
+
+ if s.Regions == nil || len(s.Regions) == 0 {
+ s.Regions = []string{
+ hostname.RegionName(),
+ }
+ }
+
+ if s.Domain == "" {
+ s.Domain = constants.SDDomain
+ }
+
+ if s.IP4 == "" || s.IP6 == "" {
+ ip4, ip6, err := dns.ResolveDualStack(s.AdvertiseHost)
+ if err != nil {
+ return fmt.Errorf("ip4 or ip6 not provided, and we were unable to resolve them")
+ }
+ if s.IP4 == "" {
+ s.IP4 = ip4
+ }
+ if s.IP6 == "" {
+ s.IP6 = ip6
+ }
+ }
+
+ if len(errors) > 0 {
+ return fmt.Errorf("errors encountered while initializing SDService:\n %s", strings.Join(errors, "\n "))
+ }
+
+ return nil
+}
+
+func (s *SDPublisher) Publish(ctx context.Context) error {
+ err := s.init()
+ if err != nil {
+ return err
+ }
+
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ if s.lease != nil {
+ // service is already published
+ return nil
+ }
+ s.ctx, s.cancel = context.WithCancel(ctx)
+
+ go s.renewalLoop()
+
+ return nil
+}
+
+func (s *SDPublisher) Unpublish() {
+ err := s.init()
+ if err != nil {
+ panic(err)
+ }
+
+ if s.lease != nil {
+ s.cancel()
+ s.wg.Wait()
+ }
+}
+
+func (s *SDPublisher) publish() error {
+ records := s.recordsToPublish()
+
+ for _, record := range records {
+ s.logger.AppendPrefix(".publish").V(1).Infof("Publishing %s record at %s: %s", record.rtype, record.path, record.val)
+
+ _, err := s.EtcdClient.Put(s.ctx,
+ record.path, record.val, etcd_client.WithLease(s.lease.ID))
+ if err != nil {
+ s.leases.Revoke(s.ctx, s.lease.ID)
+ s.lease = nil
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *SDPublisher) unpublish() {
+ s.mtx.Lock()
+ defer s.mtx.Unlock()
+
+ revokeOk := false
+ if s.lease != nil {
+ _, err := s.leases.Revoke(context.Background(), s.lease.ID)
+ if err == nil {
+ revokeOk = true
+ }
+ }
+
+ if !revokeOk {
+ records := s.recordsToPublish()
+ for _, record := range records {
+ s.EtcdClient.Delete(s.ctx, record.path)
+ }
+ }
+
+ s.lease = nil
+ s.ctx = nil
+ s.cancel = nil
+}
+
+func (s *SDPublisher) recordsToPublish() []recordToPublish {
+ records := make([]recordToPublish, 0)
+
+ records = append(records, recordToPublish{
+ rtype: "SRV",
+ path: s.srvRecordPath(),
+ val: s.srvRecordJson(s.LocalRegion),
+ })
+
+ records = append(records, recordToPublish{
+ rtype: "A",
+ path: s.aRecordPath(),
+ val: s.aRecordJson(),
+ })
+
+ records = append(records, recordToPublish{
+ rtype: "AAAA",
+ path: s.aaaaRecordPath(),
+ val: s.aaaaRecordJson(),
+ })
+
+ return records
+}
+
+func (s *SDPublisher) renewalLoop() {
+ ticker := time.NewTicker(leaseRenewalInterval * time.Second)
+ defer ticker.Stop()
+
+ s.wg.Add(1)
+ defer s.wg.Done()
+
+ // renew/grant at the start of execution
+ s.renewalTick()
+
+ for {
+ select {
+ case <-s.ctx.Done():
+ s.logger.V(1).Infof("Tearing down DNS records for service %s on shard %s", s.Service, s.ShardName)
+ s.unpublish()
+ return
+ case <-ticker.C:
+ s.renewalTick()
+ }
+ }
+}
+
+func (s *SDPublisher) renewalTick() {
+ var err error
+ var kar *etcd_client.LeaseKeepAliveResponse
+
+ kar, err = nil, nil
+ if s.lease != nil {
+ s.logger.V(3).Debugf("Doing keepalive for service %s on %s", s.Service, s.AdvertiseHost)
+ kar, err = s.leases.KeepAliveOnce(s.ctx, s.lease.ID)
+ } else {
+ s.logger.V(1).Debugf("no active lease for service %s on %s, attempting to acquire one", s.Service, s.AdvertiseHost)
+ }
+ if kar == nil || err != nil || (kar != nil && kar.ID == etcd_client.NoLease) {
+ // we lost our lease - get a new one
+ s.lease = nil
+ lease, err := s.leases.Grant(s.ctx, leaseMaxLifetime)
+ if lease == nil || err != nil {
+ s.logger.Debugf("warning: lost our lease and failed to get a new one. ")
+ return
+ }
+ s.logger.V(2).Debugf("leaseID: %016x", lease.ID)
+
+ s.lease = lease
+
+ err = s.publish()
+ if err != nil {
+ s.logger.Warningf("warning: failed to re-publish record after losing our lease: %v", err)
+ return
+ }
+ }
+}
+
+func (s *SDPublisher) srvRecordPath() string {
+ domainPathComponents := strings.Join(utils.Reverse(strings.Split(s.Domain, ".")), "/")
+ return fmt.Sprintf("/sd/%s/%s/%s/_%s/%s",
+ "dns",
+ domainPathComponents,
+ s.Protocol.DNSComponent(),
+ s.Service,
+ s.ShardName)
+}
+
+func (s *SDPublisher) srvRecordJson(region string) string {
+ prio := uint(5)
+ if region != s.LocalRegion {
+ prio = 10
+ }
+ recordValue, _ := json.Marshal(srvRecord{
+ Host: s.AdvertiseHost,
+ Port: s.AdvertisePort,
+ Priority: prio,
+ TTL: uint(leaseRenewalInterval),
+ })
+
+ return string(recordValue)
+}
+
+func (s *SDPublisher) aRecordPath() string {
+ domainPathComponents := strings.Join(utils.Reverse(strings.Split(s.Domain, ".")), "/")
+ return fmt.Sprintf("/sd/%s/%s/%s/%s/a",
+ "dns",
+ domainPathComponents,
+ s.Service,
+ s.ShardName)
+}
+
+func (s *SDPublisher) aRecordJson() string {
+ recordValue, _ := json.Marshal(addressRecord{
+ Host: s.IP4,
+ TTL: uint(leaseRenewalInterval),
+ })
+
+ return string(recordValue)
+}
+
+func (s *SDPublisher) aaaaRecordPath() string {
+ domainPathComponents := strings.Join(utils.Reverse(strings.Split(s.Domain, ".")), "/")
+ return fmt.Sprintf("/sd/%s/%s/%s/%s/aaaa",
+ "dns",
+ domainPathComponents,
+ s.Service,
+ s.ShardName)
+}
+
+func (s *SDPublisher) aaaaRecordJson() string {
+ recordValue, _ := json.Marshal(addressRecord{
+ Host: s.IP6,
+ TTL: uint(leaseRenewalInterval),
+ })
+
+ return string(recordValue)
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/sd"
+ "go.fuhry.dev/runtime/utils/hostname"
+)
+
+func main() {
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ svc := &sd.SDPublisher{}
+ var port uint
+ var protocolStr string
+ var regionsStr string
+
+ flag.StringVar(&svc.AdvertiseHost, "host", "", "The DNS name that will be advertised in the SRV record")
+ flag.UintVar(&port, "port", 0, "The port that will be advertised for the service in the SRV record")
+ flag.StringVar(&protocolStr, "proto", "tcp", "Layer 4 protocol the service uses (tcp or udp)")
+ flag.StringVar(&svc.Domain, "domain", constants.SDDomain, "domain name under which services are advertised")
+ flag.StringVar(®ionsStr, "region", hostname.RegionName(), "region(s) in which to publish the record (separate multiple regions with commas)")
+ flag.StringVar(&svc.Service, "service", "", "name of the service, like \"http\", etc.")
+ flag.StringVar(&svc.ShardName, "shard", "", "name of the shard/individual cluster member; default is derived from advertised hostname")
+ flag.Parse()
+ svc.AdvertisePort = uint16(port)
+ svc.Regions = strings.Split(regionsStr, ",")
+ l4p, err := sd.Layer4ProtocolFromString(protocolStr)
+ if err != nil {
+ panic(err)
+ }
+ svc.Protocol = l4p
+
+ err = svc.Publish(ctx)
+ if err != nil {
+ panic(err)
+ }
+
+ <-ctx.Done()
+
+ svc.Unpublish()
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "path"
+ "strings"
+ "sync"
+ "syscall"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/sd"
+ "go.fuhry.dev/runtime/utils/hostname"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type confPaths []string
+
+func (cp *confPaths) String() string {
+ return strings.Join(*cp, ";")
+}
+
+func (cp *confPaths) Set(value string) error {
+ *cp = append(*cp, value)
+ return nil
+}
+
+func main() {
+ logger := log.Default()
+
+ shutdownWaits := &sync.WaitGroup{}
+
+ confs := make(confPaths, 0)
+ var (
+ region string
+ domain string
+ )
+
+ defaultRegion := hostname.RegionName()
+
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(),
+ "%s continually monitors the health of a service, publishes it in "+
+ "the service\ndirectory as long as it's healthy, and removes registration "+
+ "when it becomes unhealthy.\n\n"+
+ "%s also reports service health under the `/sd/_health` prefix, for "+
+ "exporting\nto prometheus via `sd-health-exporter(1)`.\n\n",
+ os.Args[0], os.Args[0])
+
+ flag.PrintDefaults()
+ }
+ flag.Var(&confs, "service", "Service definition to health check; may be specified multiple times. If this is a directory, all .json files in this directory will be loaded.")
+ flag.StringVar(&domain, "domain", constants.SDDomain, "domain name to suffix all published services with")
+ flag.StringVar(®ion, "region", defaultRegion, "regional shard to publish under; separate multiple shards with commas")
+ flag.Parse()
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ if len(confs) < 1 {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ for _, p := range confs {
+ stat, err := os.Stat(p)
+ if err != nil {
+ panic(err)
+ }
+ if stat.Mode().IsRegular() {
+ processAndStart(ctx, p, domain, strings.Split(region, ","), shutdownWaits)
+ } else if stat.Mode().IsDir() {
+ entries, err := os.ReadDir(p)
+ if err != nil {
+ panic(err)
+ }
+ for _, e := range entries {
+ if e.Name()[len(e.Name())-5:] != ".json" {
+ continue
+ }
+
+ fullPath := path.Join(p, e.Name())
+ stat, err := os.Stat(fullPath)
+ if err != nil {
+ panic(err)
+ }
+ if !stat.Mode().IsRegular() {
+ panic(fmt.Errorf("json file is not a regular file: %s", fullPath))
+ }
+
+ logger.Infof("loading service file: %s", fullPath)
+
+ processAndStart(ctx, fullPath, domain, strings.Split(region, ","), shutdownWaits)
+ }
+ }
+ }
+
+ <-ctx.Done()
+
+ shutdownWaits.Wait()
+}
+
+func processAndStart(ctx context.Context, p, domain string, regions []string, shutdownWaits *sync.WaitGroup) {
+ contents, err := os.ReadFile(p)
+ if err != nil {
+ panic(err)
+ }
+
+ svc, err := sd.LoadHealthCheck(contents)
+ if err != nil {
+ panic(err)
+ }
+
+ publisher, err := sd.NewHealthCheckPublisher(svc, domain, regions)
+ if err != nil {
+ panic(err)
+ }
+ go publisher.MonitorAndPublish(ctx, shutdownWaits)
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/sd"
+)
+
+func main() {
+ var (
+ domain string
+ service string
+ protocolStr string
+ )
+
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(),
+ "%s monitors for changes to the publication of a given service.\n\n",
+ os.Args[0])
+
+ flag.PrintDefaults()
+ }
+ flag.StringVar(&domain, "domain", constants.SDDomain, "domain name to suffix all published services with")
+ flag.StringVar(&service, "service", "", "name of the service to watch")
+ flag.StringVar(&protocolStr, "proto", "tcp", "Layer 4 protocol the service uses (tcp or udp)")
+ flag.Parse()
+
+ l4p, err := sd.Layer4ProtocolFromString(protocolStr)
+ if err != nil {
+ panic(err)
+ }
+
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ watcher := &sd.SDWatcher{
+ Domain: domain,
+ Protocol: l4p,
+ Service: service,
+ }
+
+ ticker := time.NewTicker(15 * time.Second)
+ defer ticker.Stop()
+
+ addrs, err := watcher.GetAddrs(ctx)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("items: %v\n", addrs)
+
+ updates := watcher.WatchUpdates(ctx)
+
+mainLoop:
+ for {
+ select {
+ case addrs = <-updates:
+ fmt.Printf("items updated: %v\n", addrs)
+ case <-ticker.C:
+ fmt.Printf("items have not changed: %v\n", addrs)
+ case <-ctx.Done():
+ break mainLoop
+ }
+ }
+}
--- /dev/null
+[Unit]
+Description=Export Prometheus metrics for service health
+Wants=systemd-networkd-wait-online.service
+After=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+User=node_exporter
+Group=node_exporter
+Environment=MTLS_IDENTITY=node-exporter
+ExecStart=/usr/bin/mtls.id=${MTLS_IDENTITY}
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+[Unit]
+Description=Service discovery and health checking for %i
+Wants=systemd-networkd-wait-online.service
+After=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+User=nobody
+Group=nobody
+ExecStart=/usr/bin/mtls.id=etcd-client -service /etc/runtime/sd
+
+[Install]
+WantedBy=multi-user.target
+
--- /dev/null
+[Unit]
+Description=Service discovery and health checking for %i
+Wants=systemd-networkd-wait-online.service
+After=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+User=nobody
+Group=nobody
+ExecStart=/usr/bin/sd-register -mtls.id=etcd-client -service /etc/runtime/sd/%i -domain %i
+
+[Install]
+WantedBy=multi-user.target
+
--- /dev/null
+package sd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+
+ etcd_client "go.etcd.io/etcd/client/v3"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/net/dns"
+ "go.fuhry.dev/runtime/utils"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type SDWatcher struct {
+ Region string
+ Domain string
+ Protocol Layer4Protocol
+ Service string
+ EtcdClient *etcd_client.Client
+
+ logger *log.Logger
+ starter *sync.Once
+ lock *sync.RWMutex
+ wg *sync.WaitGroup
+ results []ServiceAddress
+ watchers chan []ServiceAddress
+ watcherCount int
+}
+
+type ServiceAddress struct {
+ Hostname string
+ Port uint16
+ IP4 string
+ IP6 string
+ Protocol Layer4Protocol
+ Service string
+ Shard string
+
+ key string
+}
+
+func (w *SDWatcher) init() error {
+ if w.Region == "" {
+ w.Region = "dns"
+ }
+
+ if w.Domain == "" {
+ w.Domain = constants.SDDomain
+ }
+
+ // if w.Service == "" {
+ // return fmt.Errorf("service name must be specified")
+ // }
+
+ if w.EtcdClient == nil {
+ cl, err := NewDefaultEtcdClient()
+ if err != nil {
+ return err
+ }
+
+ w.EtcdClient = cl
+ }
+
+ if w.lock == nil {
+ w.watchers = make(chan []ServiceAddress, 10)
+ w.starter = &sync.Once{}
+ w.lock = &sync.RWMutex{}
+ w.wg = &sync.WaitGroup{}
+ w.logger = log.WithPrefix("SDWatcher:" + w.Service)
+ }
+
+ return nil
+}
+
+func (w *SDWatcher) GetAddrs(ctx context.Context) ([]ServiceAddress, error) {
+ err := w.init()
+ if err != nil {
+ return nil, err
+ }
+
+ w.starter.Do(func() {
+ go w.watch(ctx)
+
+ w.wg.Add(1)
+ w.wg.Wait()
+ })
+
+ if len(w.results) < 1 {
+ return nil, fmt.Errorf("failed to discover any instances of service %q in domain %q and region %q", w.Service, w.Domain, w.Region)
+ }
+
+ return w.results, nil
+}
+
+func (w *SDWatcher) watch(ctx context.Context) {
+ kvs := make(map[string][]byte, 0)
+
+ w.logger.Infof("Watching for service publications under path %s", w.prefix())
+
+ items, err := w.EtcdClient.Get(ctx, w.prefix(), etcd_client.WithPrefix())
+ if err == nil {
+ for _, kv := range items.Kvs {
+ kvs[string(kv.Key)] = kv.Value
+ }
+
+ w.publishResults(kvs)
+ w.wg.Done()
+
+ }
+
+ watcher := etcd_client.NewWatcher(w.EtcdClient)
+ keyWatch := watcher.Watch(ctx, w.prefix(), etcd_client.WithPrefix())
+
+ for {
+ select {
+ case items := <-keyWatch:
+ for _, ev := range items.Events {
+ if ev.Type.String() == "PUT" {
+ w.logger.V(2).Debugf("%s was published", string(ev.Kv.Key))
+ kvs[string(ev.Kv.Key)] = ev.Kv.Value
+ } else if ev.Type.String() == "DELETE" {
+ w.logger.V(2).Debugf("%s was deleted", string(ev.Kv.Key))
+ delete(kvs, string(ev.Kv.Key))
+ }
+ }
+
+ w.publishResults(kvs)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func (w *SDWatcher) publishResults(kvs map[string][]byte) {
+ w.lock.Lock()
+ w.results = w.buildResult(kvs)
+ w.lock.Unlock()
+
+ for i := 0; i < w.watcherCount; i++ {
+ w.watchers <- w.results
+ }
+}
+
+func (w *SDWatcher) WatchUpdates(ctx context.Context) chan []ServiceAddress {
+ w.watcherCount++
+ och := make(chan []ServiceAddress)
+
+ go (func() {
+ defer func() { w.watcherCount-- }()
+
+ for {
+ select {
+ case m := <-w.watchers:
+ och <- m
+ case <-ctx.Done():
+ close(och)
+ return
+ }
+ }
+ })()
+
+ return och
+}
+
+func (w *SDWatcher) prefix() string {
+ domainComponents := utils.Reverse(strings.Split(w.Domain, "."))
+ domainPath := strings.Join(domainComponents, "/")
+
+ s := fmt.Sprintf("/sd/%s/%s/", w.Region, domainPath)
+ if w.Protocol != ProtocolInvalid {
+ s += fmt.Sprintf("%s/", w.Protocol.DNSComponent())
+ if w.Service != "" {
+ s += fmt.Sprintf("_%s/", w.Service)
+ }
+ } else {
+ s += "_"
+ }
+ return s
+}
+
+func (w *SDWatcher) buildResult(kvs map[string][]byte) []ServiceAddress {
+ sas := make([]ServiceAddress, 0)
+ record := &srvRecord{}
+
+ for key, value := range kvs {
+ err := json.Unmarshal(value, record)
+ if err != nil {
+ continue
+ }
+ ip4, ip6, err := dns.ResolveDualStack(record.Host)
+ if err != nil {
+ continue
+ }
+ s := fmt.Sprintf("/sd/%s/%s/", w.Region, w.Domain)
+ components := strings.Split(key[len(s):], "/")
+ sa := ServiceAddress{
+ Hostname: record.Host,
+ Port: record.Port,
+ IP4: ip4,
+ IP6: ip6,
+ key: key[len(s):],
+ }
+ if len(components) == 3 {
+ sa.Protocol, _ = Layer4ProtocolFromString(components[0])
+ sa.Service = components[1][1:]
+ sa.Shard = components[2]
+ }
+ sas = append(sas, sa)
+ }
+
+ return sas
+}
+
+func (sa ServiceAddress) ToURI() *url.URL {
+ return &url.URL{
+ Scheme: "sd",
+ Host: sa.Service,
+ Path: fmt.Sprintf("/%s", sa.Protocol.String()),
+ }
+}
--- /dev/null
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+ rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+ go build $(GOBUILDFLAGS) -o $@ $<
+
--- /dev/null
+version: 0.1
+log:
+ fields:
+ service: registry
+storage:
+ cache:
+ blobdescriptor: inmemory
+ filesystem:
+ rootdirectory: /var/lib/registry
+http:
+ addr: :5005
+ host: https://registry.example.com/
+ headers:
+ X-Content-Type-Options: [nosniff]
+ draintimeout: 60s
+health:
+ storagedriver:
+ enabled: true
+ interval: 10s
+ threshold: 3
+
--- /dev/null
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ _ "net/http/pprof"
+ "os"
+ "os/signal"
+ "strconv"
+ "syscall"
+
+ "github.com/distribution/distribution/v3/configuration"
+ _ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
+ _ "github.com/distribution/distribution/v3/registry/auth/silly"
+ _ "github.com/distribution/distribution/v3/registry/auth/token"
+ "github.com/distribution/distribution/v3/registry/handlers"
+ _ "github.com/distribution/distribution/v3/registry/proxy"
+ _ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
+ _ "github.com/distribution/distribution/v3/registry/storage/driver/oss"
+
+ // proxyproto "github.com/pires/go-proxyproto"
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/grpc"
+ "go.fuhry.dev/runtime/metrics/metricbus/mbclient"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/sd"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+var logger *log.Logger
+var DefaultSSLCertificate = "registry." + constants.WebServicesDomain
+
+const (
+ privilegedIdentity = "docker-publish"
+)
+
+type registryMetrics struct {
+ requests mbclient.CounterMetric
+ requestsXX mbclient.CounterMetric
+}
+
+func main() {
+ var port uint
+ var sslCert string
+ var configPath string
+ flag.UintVar(&port, "registry.port", grpc.RandomPort(), "port to listen on")
+ flag.StringVar(&sslCert, "registry.ssl-cert", DefaultSSLCertificate, "ssl certificate to load from secrets")
+ flag.StringVar(&configPath, "registry.config-path", "config.yml", "path to configuration file")
+ flag.Parse()
+ logger = log.WithPrefix("registry")
+ ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+ metricsService := mbclient.NewService(ctx)
+ defer metricsService.FlushAndWait()
+
+ metrics := ®istryMetrics{
+ requests: metricsService.DefineCounter("registry_http_requests", "HTTP requests received and processed by the container registry", "response_code"),
+ requestsXX: metricsService.DefineCounter("registry_http_requests_xx", "HTTP requests received and processed by the container registry, broken down by response code class (first digit of status code)", "response_code_class"),
+ }
+
+ config, err := resolveConfiguration([]string{configPath})
+ if err != nil {
+ logger.Fatal(err)
+ }
+
+ tlsListener, err := makeTlsListener(port, sslCert, ctx)
+ if err != nil {
+ logger.Fatal(err)
+ }
+
+ // proxyProtoListener := &proxyproto.Listener{
+ // Listener: tlsListener,
+ // ReadHeaderTimeout: 10 * time.Second,
+ // }
+
+ handler := http.NewServeMux()
+ app := handlers.NewApp(ctx, config)
+ handler.Handle("/", metricsMiddleware(registryAuthMiddleware(app), metrics))
+ handler.HandleFunc("/ready", func(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(200)
+ w.Write([]byte("LIVE"))
+ })
+
+ server := &http.Server{
+ Handler: handler,
+ }
+
+ logger.Noticef("will listen on port %d", port)
+
+ publisher := &sd.SDPublisher{
+ AdvertisePort: uint16(port),
+ Protocol: sd.ProtocolTCP,
+ Service: "distribution",
+ }
+
+ go (func() {
+ err := server.Serve(tlsListener)
+ if err != nil {
+ logger.Fatal(err)
+ }
+ })()
+
+ publisher.Publish(ctx)
+
+ <-ctx.Done()
+
+ publisher.Unpublish()
+ if config.HTTP.DrainTimeout > 0 {
+ logger.Notice("shutdown requested, draining for up to ", config.HTTP.DrainTimeout)
+ drainCtx, cancel := context.WithTimeout(context.Background(), config.HTTP.DrainTimeout)
+ server.Shutdown(drainCtx)
+ cancel()
+ }
+}
+
+func resolveConfiguration(args []string) (*configuration.Configuration, error) {
+ var configurationPath string
+
+ if len(args) > 0 {
+ configurationPath = args[0]
+ } else if os.Getenv("REGISTRY_CONFIGURATION_PATH") != "" {
+ configurationPath = os.Getenv("REGISTRY_CONFIGURATION_PATH")
+ }
+
+ if configurationPath == "" {
+ return nil, fmt.Errorf("configuration path unspecified")
+ }
+
+ fp, err := os.Open(configurationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ defer fp.Close()
+
+ config, err := configuration.Parse(fp)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing %s: %v", configurationPath, err)
+ }
+
+ return config, nil
+}
+
+func makeTlsListener(port uint, sslCert string, ctx context.Context) (net.Listener, error) {
+ cert := mtls.NewSSLCertificate(sslCert)
+ tlsConfig, err := cert.TlsConfig(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ return nil, err
+ }
+ netListener, err := net.ListenTCP("tcp", tcpAddr)
+ if err != nil {
+ return nil, err
+ }
+
+ cv := mtls.NewPeerNameVerifier()
+ cv.AllowFrom(mtls.Service, privilegedIdentity)
+ cv.AllowFrom(mtls.Service, "docker-pull")
+ // to allow healthchecking and metrics querying
+ cv.AllowFrom(mtls.Service, "prometheus")
+ cv.ConfigureServer(tlsConfig)
+
+ return tls.NewListener(netListener, tlsConfig), nil
+}
+
+func registryAuthMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ remoteIdentity, err := mtls.IdentityFromTLSConnectionState(req.TLS)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte(
+ fmt.Sprintf("Failed to get identity from TLS connection state: %+v", err)))
+ return
+ }
+ if req.Method != "GET" && req.Method != "HEAD" && req.Method != "OPTIONS" {
+ if remoteIdentity.Name() != privilegedIdentity {
+ w.WriteHeader(401)
+ w.Write([]byte(
+ fmt.Sprintf("Unauthorized: only %s may perform %s requests", privilegedIdentity, req.Method)))
+ }
+ }
+ next.ServeHTTP(w, req)
+ })
+}
+
+type loggingResponseWriter struct {
+ http.ResponseWriter
+ statusCode int
+}
+
+func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
+ return &loggingResponseWriter{w, 200}
+}
+
+func (lrw *loggingResponseWriter) WriteHeader(code int) {
+ lrw.statusCode = code
+ lrw.ResponseWriter.WriteHeader(code)
+}
+
+func metricsMiddleware(next http.Handler, metrics *registryMetrics) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ lrw := newLoggingResponseWriter(w)
+
+ next.ServeHTTP(lrw, req)
+
+ statusCode := strconv.Itoa(lrw.statusCode)
+ metrics.requests.WithLabelValues(mbclient.KV{"response_code": statusCode}).Add(1)
+ metrics.requestsXX.WithLabelValues(mbclient.KV{"response_code_class": statusCode[:1]}).Add(1)
+ })
+}
--- /dev/null
+[Unit]
+Description=Docker registry wrapper
+Wants=systemd-networkd-wait-online.service
+After=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+User=registry
+Group=registry
+ExecStart=/usr/bin/docker-registry -mtls.id=distribution -registry.port=5005 -registry.config-path=/etc/runtime/docker-registry.yml
+
+[Install]
+WantedBy=multi-user.target
+
--- /dev/null
+package ansi
+
+import (
+ "fmt"
+ "strings"
+)
+
+type Color uint
+
+const Escape byte = 0x1B
+
+const (
+ Reset Color = 0
+ Bold Color = 1
+
+ Black Color = 30
+ Red Color = 31
+ Green Color = 32
+ Yellow Color = 33
+ Blue Color = 34
+ Purple Color = 35
+ Cyan Color = 36
+ White Color = 37
+
+ BgBlack Color = 40
+ BgRed Color = 41
+ BgGreen Color = 42
+ BgYellow Color = 43
+ BgBlue Color = 44
+ BgPurple Color = 45
+ BgCyan Color = 46
+ BgWhite Color = 47
+)
+
+func String(seq ...Color) string {
+ s := string([]byte{Escape}) + "["
+ seqSlice := make([]string, len(seq))
+ for i, color := range seq {
+ seqSlice[i] = fmt.Sprintf("%d", color)
+ }
+ s += strings.Join(seqSlice, ";") + "m"
+
+ return s
+}
--- /dev/null
+package debounce
+
+import (
+ "context"
+ "time"
+)
+
+type actionFunc func()
+type Debounce struct {
+ action actionFunc
+ timeout time.Duration
+ ticker time.Ticker
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+var DefaultTimeout = 50 * time.Millisecond
+
+func New(action actionFunc) *Debounce {
+ return NewWithTimeout(action, DefaultTimeout)
+}
+
+func NewWithTimeout(action actionFunc, timeout time.Duration) *Debounce {
+ return &Debounce{
+ action: action,
+ timeout: timeout,
+ }
+}
+
+func (d *Debounce) Trigger() {
+ if d.ctx != nil {
+ d.cancel()
+ }
+ d.ctx, d.cancel = context.WithTimeout(context.TODO(), d.timeout)
+ d.ticker = *time.NewTicker(d.timeout)
+
+ go d.maybeAction()
+}
+
+func (d *Debounce) maybeAction() {
+ select {
+ case <-d.ctx.Done():
+ return
+ case <-d.ticker.C:
+ d.action()
+ }
+}
--- /dev/null
+package debounce
+
+import (
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type DebounceSuite struct{}
+
+var _ = Suite(&DebounceSuite{})
+
+func (s *DebounceSuite) TestSingleTrigger(c *C) {
+ var counter uint32 = 0
+ action := func() {
+ counter++
+ }
+
+ d := NewWithTimeout(action, 20*time.Millisecond)
+ d.Trigger()
+
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(10 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(15 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(1))
+}
+
+func (s *DebounceSuite) TestMultiTrigger(c *C) {
+ var counter uint32 = 0
+ action := func() {
+ counter++
+ }
+
+ d := NewWithTimeout(action, 20*time.Millisecond)
+ d.Trigger()
+ d.Trigger()
+ d.Trigger()
+
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(10 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(15 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(1))
+}
+
+func (s *DebounceSuite) TestDelayTrigger(c *C) {
+ var counter uint32 = 0
+ action := func() {
+ counter++
+ }
+
+ d := NewWithTimeout(action, 20*time.Millisecond)
+
+ d.Trigger()
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(10 * time.Millisecond)
+
+ d.Trigger()
+ c.Assert(counter, Equals, uint32(0))
+ time.Sleep(15 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(0))
+
+ d.Trigger()
+ time.Sleep(25 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(1))
+
+ d.Trigger()
+ c.Assert(counter, Equals, uint32(1))
+ time.Sleep(25 * time.Millisecond)
+ c.Assert(counter, Equals, uint32(2))
+}
--- /dev/null
+package generics
+
+type number interface {
+ ~int | ~int8 | ~int16 | ~int32 | ~int64 |
+ ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
+ ~float32 | ~float64
+}
+
+func Min[Tcomp number](n ...Tcomp) Tcomp {
+ if len(n) < 1 {
+ return Tcomp(0)
+ }
+ m := n[0]
+ for _, i := range n {
+ if i < m {
+ m = i
+ }
+ }
+
+ return m
+}
+
+func Max[Tcomp number](n ...Tcomp) Tcomp {
+ if len(n) < 1 {
+ return Tcomp(0)
+ }
+
+ m := n[0]
+ for _, i := range n {
+ if i > m {
+ m = i
+ }
+ }
+
+ return m
+}
--- /dev/null
+package hashset
+
+import (
+ "sync"
+)
+
+type Hashable = comparable
+
+type HashSet[TKey Hashable] struct {
+ data map[TKey]interface{}
+ lock *sync.RWMutex
+}
+
+func NewHashSet[TKey Hashable]() *HashSet[TKey] {
+ data := make(map[TKey]interface{}, 0)
+ return &HashSet[TKey]{
+ data: data,
+ lock: &sync.RWMutex{},
+ }
+}
+
+func FromSlice[TKey Hashable](inp []TKey) *HashSet[TKey] {
+ hs := NewHashSet[TKey]()
+ hs.Add(inp...)
+ return hs
+}
+
+func (hs *HashSet[TKey]) Add(items ...TKey) {
+ hs.lock.Lock()
+ defer hs.lock.Unlock()
+
+ for _, e := range items {
+ hs.add(e)
+ }
+}
+
+func (hs *HashSet[TKey]) add(e TKey) {
+ hs.data[e] = nil
+}
+
+func (hs *HashSet[TKey]) Each() chan TKey {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+
+ return hs.each()
+}
+
+func (hs *HashSet[TKey]) each() chan TKey {
+ ch := make(chan TKey)
+
+ go (func() {
+ for k, _ := range hs.data {
+ ch <- k
+ }
+
+ close(ch)
+ })()
+
+ return ch
+}
+
+func (hs *HashSet[TKey]) Empty() {
+ hs.lock.Lock()
+ defer hs.lock.Unlock()
+
+ hs.empty()
+}
+
+func (hs *HashSet[TKey]) empty() {
+ hs.data = make(map[TKey]interface{}, 0)
+}
+
+func (hs *HashSet[TKey]) Union(other *HashSet[TKey]) {
+ hs.lock.Lock()
+ defer hs.lock.Unlock()
+
+ hs.union(other)
+}
+
+func (hs *HashSet[TKey]) union(other *HashSet[TKey]) {
+ for e := range other.Each() {
+ hs.add(e)
+ }
+}
+
+func (hs *HashSet[TKey]) Diff(other *HashSet[TKey]) {
+ hs.lock.Lock()
+ defer hs.lock.Unlock()
+
+ hs.diff(other)
+}
+
+func (hs *HashSet[TKey]) diff(other *HashSet[TKey]) {
+ for e := range other.Each() {
+ hs.del(e)
+ }
+}
+
+func (hs *HashSet[TKey]) Del(items ...TKey) {
+ hs.lock.Lock()
+ defer hs.lock.Unlock()
+
+ for _, e := range items {
+ hs.del(e)
+ }
+}
+
+func (hs *HashSet[TKey]) del(e TKey) {
+ if _, ok := hs.data[e]; ok {
+ delete(hs.data, e)
+ }
+}
+
+func (hs *HashSet[TKey]) AsSlice() []TKey {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+
+ return hs.asSlice()
+}
+
+func (hs *HashSet[TKey]) asSlice() []TKey {
+ s := make([]TKey, len(hs.data))
+
+ i := 0
+ for k, _ := range hs.data {
+ s[i] = k
+ i++
+ }
+
+ return s
+}
+
+func (hs *HashSet[TKey]) Len() int {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+
+ return hs.len()
+}
+
+func (hs *HashSet[TKey]) len() int {
+ return len(hs.data)
+}
+
+func (hs *HashSet[TKey]) Contains(e TKey) bool {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+
+ return hs.contains(e)
+}
+
+func (hs *HashSet[TKey]) contains(e TKey) bool {
+ _, ok := hs.data[e]
+ return ok
+}
+
+func (hs *HashSet[TKey]) Dup() *HashSet[TKey] {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+
+ return hs.dup()
+}
+
+func (hs *HashSet[TKey]) dup() *HashSet[TKey] {
+ return FromSlice(hs.AsSlice())
+}
--- /dev/null
+package hashset
+
+import (
+ "testing"
+
+ "golang.org/x/exp/slices"
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type HashSetSuite struct{}
+
+var _ = Suite(&HashSetSuite{})
+
+func (s *HashSetSuite) TestEach(c *C) {
+ items := []string{
+ "a",
+ "b",
+ "c",
+ }
+ hs := NewHashSet[string]()
+
+ hs.Add(items...)
+
+ for e := range hs.Each() {
+ c.Assert(slices.Contains(items, e), Equals, true)
+ }
+ c.Assert(hs.Len(), Equals, 3)
+}
+
+func (s *HashSetSuite) TestAsSlice(c *C) {
+ items := []string{
+ "a",
+ "b",
+ "c",
+ }
+ hs := NewHashSet[string]()
+
+ hs.Add(items...)
+
+ hsSlice := hs.AsSlice()
+
+ for _, e := range items {
+ c.Assert(slices.Contains(hsSlice, e), Equals, true)
+ }
+ c.Assert(hs.Len(), Equals, 3)
+}
+
+func (s *HashSetSuite) TestUnion(c *C) {
+ items_a := []string{
+ "a",
+ "b",
+ "c",
+ }
+
+ items_b := []string{
+ "d",
+ "e",
+ "f",
+ }
+
+ hs_a := NewHashSet[string]()
+ hs_a.Add(items_a...)
+
+ hs_b := NewHashSet[string]()
+ hs_b.Add(items_b...)
+
+ hs_a.Union(hs_b)
+
+ c.Assert(hs_a.Len(), Equals, 6)
+ for e := range hs_a.Each() {
+ c.Assert(slices.Contains(items_a, e) || slices.Contains(items_b, e), Equals, true)
+ }
+}
+
+func (s *HashSetSuite) TestDifference(c *C) {
+ items_a := []string{
+ "a",
+ "b",
+ "c",
+ }
+
+ items_b := []string{
+ "c",
+ "d",
+ "e",
+ }
+
+ hs_a := NewHashSet[string]()
+ hs_a.Add(items_a...)
+
+ hs_b := NewHashSet[string]()
+ hs_b.Add(items_b...)
+
+ hs_a.Diff(hs_b)
+
+ c.Assert(hs_a.Len(), Equals, 2)
+ hsSlice := hs_a.AsSlice()
+ c.Assert(slices.Contains(hsSlice, "a"), Equals, true)
+ c.Assert(slices.Contains(hsSlice, "b"), Equals, true)
+ c.Assert(slices.Contains(hsSlice, "c"), Equals, false)
+}
--- /dev/null
+//go:build !darwin
+
+package hostname
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+ "syscall"
+)
+
+type i8 = interface {
+ int8 | uint8
+}
+
+var utsname syscall.Utsname
+var utsnameOnce sync.Once
+
+func Hostname() string {
+ return strings.Split(nodeName(), ".")[0]
+}
+
+func DomainName() string {
+ uname := uname()
+
+ domainName := int8ToString(uname.Domainname)
+ if domainName != "(none)" {
+ return domainName
+ }
+
+ nodeName := int8ToString(uname.Nodename)
+ for i, chr := range []byte(nodeName) {
+ if chr == '.' {
+ return nodeName[i+1:]
+ }
+ }
+
+ err := fmt.Errorf("could not determine domain name from (uname.Nodename=%v) (uname.Domainname=%v)", uname.Nodename, uname.Domainname)
+ panic(err)
+}
+
+func RegionName() string {
+ domain := DomainName()
+
+ if domain == "" {
+ panic("domain string is empty")
+ }
+
+ return strings.Split(domain, ".")[0]
+}
+
+func Fqdn() string {
+ return strings.Join([]string{Hostname(), DomainName()}, ".")
+}
+
+func uname() syscall.Utsname {
+ utsnameOnce.Do(func() {
+ err := syscall.Uname(&utsname)
+ if err != nil {
+ panic(err)
+ }
+ })
+
+ return utsname
+}
+
+func nodeName() string {
+ uname := uname()
+
+ return int8ToString(uname.Nodename)
+}
+
+func int8ToString[T i8](ba [65]T) string {
+ bytes := make([]byte, 0)
+ for _, b := range ba {
+ if b == T(0) {
+ break
+ }
+ bytes = append(bytes, byte(b))
+ }
+
+ return string(bytes)
+}
--- /dev/null
+//go:build darwin
+
+package hostname
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/log"
+ "howett.net/plist"
+)
+
+const (
+ defaultDomainName = constants.DefaultHostDomain
+
+ systemPreferencesPlist = "/Library/Preferences/SystemConfiguration/preferences.plist"
+)
+
+type systemPrefs struct {
+ System *systemPrefs_System `plist:"System"`
+}
+
+type systemPrefs_System struct {
+ Network *systemPrefs_System_Network `plist:"Network"`
+ System *systemPrefs_System_System `plist:"System"`
+}
+
+type systemPrefs_System_Network struct {
+ HostNames *systemPrefs_System_Network_HostNames `plist:"HostNames"`
+}
+
+type systemPrefs_System_Network_HostNames struct {
+ LocalHostName string `plist:"LocalHostName"`
+}
+
+type systemPrefs_System_System struct {
+ ComputerName string `plist:"ComputerName"`
+ ComputerNameEncoding int `plist:"ComputerNameEncoding"`
+}
+
+func Hostname() string {
+ fqdn := Fqdn()
+ return strings.Split(fqdn, ".")[0]
+}
+
+func DomainName() string {
+ fqdn := strings.Split(Fqdn(), ".")
+ if len(fqdn) > 1 {
+ return strings.Join(fqdn[1:], ".")
+ }
+ return defaultDomainName
+}
+
+func RegionName() string {
+ return strings.Split(DomainName(), ".")[0]
+}
+
+func Fqdn() string {
+ dnsFqdn, dnsErr := fqdnFromDns()
+ if dnsErr == nil {
+ return maybeAppendDefaultDomainName(dnsFqdn)
+ }
+
+ prefsFqdn, prefsErr := fqdnFromSystemPreferences()
+ if prefsErr == nil {
+ return maybeAppendDefaultDomainName(prefsFqdn)
+ }
+
+ log.Panicf("failed to get fqdn: dns failed because %q and prefs failed because %q", dnsErr.Error(), prefsErr.Error())
+ return ""
+}
+
+func maybeAppendDefaultDomainName(name string) string {
+ parts := strings.Split(name, ".")
+ if len(parts) < 2 {
+ name = fmt.Sprintf("%s.%s", parts[0], defaultDomainName)
+ }
+
+ log.Default().V(1).Noticef("detected system hostname: %q", name)
+
+ return name
+}
+
+func fqdnFromSystemPreferences() (string, error) {
+ fp, err := os.Open(systemPreferencesPlist)
+ if err != nil {
+ return "", err
+ }
+ defer fp.Close()
+
+ decoder := plist.NewDecoder(fp)
+
+ prefs := &systemPrefs{}
+ err = decoder.Decode(prefs)
+ if err != nil {
+ return "", err
+ }
+
+ name := ""
+
+ if n := prefs.System.Network.HostNames.LocalHostName; n != "" {
+ name = n
+ }
+ if n := prefs.System.System.ComputerName; n != "" && (name == "" || name == "localhost" || strings.HasPrefix(name, "localhost.")) {
+ name = n
+ }
+
+ if name == "" {
+ return "", fmt.Errorf("failed to read system hostname from %s: all supported properties came up empty", systemPreferencesPlist)
+ }
+
+ if name == "localhost" || strings.HasPrefix(name, "localhost.") {
+ return "", fmt.Errorf("successfully read hostname from %q, but hostname is 'localhost'", systemPreferencesPlist)
+ }
+
+ return name, nil
+}
+
+func fqdnFromDns() (string, error) {
+ resolver := &net.Resolver{}
+
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ result, err := resolver.LookupAddr(ctx, "127.0.0.1")
+ if err != nil {
+ return "", fmt.Errorf("failed to lookup hostname for 127.0.0.1: %v", err)
+ }
+
+ for _, name := range result {
+ parts := strings.Split(name, ".")
+ if parts[0] != "localhost" {
+ return parts[0], nil
+ }
+ }
+
+ return "", errors.New("failed to lookup any hostname for 127.0.0.1 that does not resolve to just \"localhost\"")
+}
--- /dev/null
+package log
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "go.fuhry.dev/runtime/utils/ansi"
+)
+
+type Level uint
+
+const (
+ noLevel Level = iota
+ DEBUG
+ INFO
+ NOTICE
+ WARNING
+ ERROR
+ CRITICAL
+ ALERT
+ FATAL
+)
+
+type levelFormat struct {
+ tag string
+ color []ansi.Color
+}
+
+var format = map[Level]levelFormat{
+ DEBUG: {tag: "DBG", color: []ansi.Color{ansi.Bold, ansi.Purple}},
+ INFO: {tag: "INF", color: []ansi.Color{ansi.Bold, ansi.Cyan}},
+ NOTICE: {tag: "NTC", color: []ansi.Color{ansi.Bold, ansi.Blue}},
+ WARNING: {tag: "WRN", color: []ansi.Color{ansi.Bold, ansi.Yellow}},
+ ERROR: {tag: "ERR", color: []ansi.Color{ansi.Bold, ansi.Red}},
+ CRITICAL: {tag: "CRI", color: []ansi.Color{ansi.Bold, ansi.Red, ansi.BgYellow}},
+ ALERT: {tag: "ALR", color: []ansi.Color{ansi.Bold, ansi.White, ansi.BgRed}},
+ FATAL: {tag: "FTL", color: []ansi.Color{ansi.Bold, ansi.Yellow, ansi.BgRed}},
+}
+
+func (l Level) prefix(w io.Writer) string {
+ file, ok := w.(*os.File)
+ if !ok {
+ return l.plainPrefix()
+ }
+ if !terminal.IsTerminal(int(file.Fd())) {
+ return l.plainPrefix()
+ }
+
+ f, ok := format[l]
+ if !ok {
+ return l.plainPrefix()
+ }
+
+ return fmt.Sprintf("[%s%s%s%s] ",
+ ansi.String(ansi.Reset),
+ ansi.String(f.color...),
+ f.tag,
+ ansi.String(ansi.Reset))
+}
+
+func (l Level) plainPrefix() string {
+ f, ok := format[l]
+ if !ok {
+ return ""
+ }
+
+ return fmt.Sprintf("[%s] ", f.tag)
+}
+
+func (l Level) String() string {
+ switch l {
+ case DEBUG:
+ return "debug"
+ case INFO:
+ return "info"
+ case NOTICE:
+ return "notice"
+ case WARNING:
+ return "warning"
+ case ERROR:
+ return "error"
+ case CRITICAL:
+ return "critical"
+ case ALERT:
+ return "alert"
+ case FATAL:
+ return "fatal"
+
+ default:
+ return "none"
+ }
+}
+
+func LevelFromString(s string) Level {
+ switch strings.ToLower(s) {
+ case "debug", "dbg", "d":
+ return DEBUG
+ case "info", "i":
+ return INFO
+ case "notice", "note", "not", "n":
+ return NOTICE
+ case "warning", "warn", "w":
+ return WARNING
+ case "error", "err", "e":
+ return ERROR
+ case "critical", "crit", "c":
+ return CRITICAL
+ case "alert", "a":
+ return ALERT
+ case "fatal", "ftl", "panic", "f", "p":
+ return FATAL
+ default:
+ return noLevel
+ }
+}
--- /dev/null
+package log
+
+import (
+ "flag"
+ "io"
+ "log"
+)
+
+var logVerbosity int = 0
+var logLevel string = "notice"
+
+type Logger struct {
+ *log.Logger
+
+ level Level
+ prefix string
+}
+
+func init() {
+ flag.IntVar(&logVerbosity, "vv", 0, "verbosity level for logs")
+ flag.StringVar(&logLevel, "v", INFO.String(), "syslog log level for logs")
+}
+
+func V(level int) *Logger {
+ if logVerbosity >= level {
+ return Default()
+ }
+
+ return NullLogger()
+}
+
+func Default() *Logger {
+ return &Logger{Logger: log.Default()}
+}
+
+func WithPrefix(prefix string) *Logger {
+ return Default().WithPrefix(prefix)
+}
+
+func NullLogger() *Logger {
+ return &Logger{Logger: log.New(io.Discard, "", 0)}
+}
+
+func Print(v ...any) {
+ Default().Print(v...)
+}
+
+func Printf(fmtstr string, args ...any) {
+ Default().Printf(fmtstr, args...)
+}
+
+func Println(v ...any) {
+ Default().Println(v...)
+}
+
+func Fatal(v ...any) {
+ Default().Fatal(v...)
+}
+
+func Fatalf(fmtstr string, args ...any) {
+ Default().Fatalf(fmtstr, args...)
+}
+
+func Panic(v ...any) {
+ Default().Panic(v...)
+}
+
+func Panicf(fmtstr string, v ...any) {
+ Default().Panicf(fmtstr, v...)
+}
+
+func Panicln(v ...any) {
+ Default().Panicln(v...)
+}
+
+func (l *Logger) V(level int) *Logger {
+ if logVerbosity >= level {
+ return l
+ }
+ return NullLogger()
+}
+
+func (l *Logger) WithPrefix(prefix string) *Logger {
+ return &Logger{
+ Logger: l.Logger,
+ prefix: prefix,
+ level: l.level,
+ }
+}
+
+func (l Logger) WithLevel(lv Level) *Logger {
+ if lv < LevelFromString(logLevel) {
+ return NullLogger()
+ }
+
+ return &Logger{
+ Logger: l.Logger,
+ prefix: l.prefix,
+ level: lv,
+ }
+}
+
+func (l *Logger) AppendPrefix(prefix string) *Logger {
+ return &Logger{
+ Logger: l.Logger,
+ prefix: l.prefix + prefix,
+ level: l.level,
+ }
+}
+
+func (l *Logger) prependPrefixes(v []any) []any {
+ prefix := ""
+ if l.prefix != "" {
+ prefix = "[" + l.prefix + "] "
+ }
+ args := make([]any, len(v)+2)
+ args[0] = l.level.prefix(l.Writer())
+ args[1] = prefix
+ copy(args[2:], v)
+
+ return args
+}
+
+func (l *Logger) Print(v ...any) {
+ args := l.prependPrefixes(v)
+ l.Logger.Print(args...)
+}
+
+func (l *Logger) Printf(fmtstr string, v ...any) {
+ if l.prefix != "" {
+ fmtstr = "[" + l.prefix + "] " + fmtstr
+ }
+
+ l.Logger.Printf(l.level.prefix(l.Writer())+fmtstr, v...)
+}
+
+func (l *Logger) Println(v ...any) {
+ l.Print(v...)
+}
+
+func (l *Logger) Debug(v ...any) {
+ l.WithLevel(DEBUG).Print(v...)
+}
+
+func (l *Logger) Debugf(fmtstr string, v ...any) {
+ l.WithLevel(DEBUG).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Info(v ...any) {
+ l.WithLevel(INFO).Print(v...)
+}
+
+func (l *Logger) Infof(fmtstr string, v ...any) {
+ l.WithLevel(INFO).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Notice(v ...any) {
+ l.WithLevel(NOTICE).Print(v...)
+}
+
+func (l *Logger) Noticef(fmtstr string, v ...any) {
+ l.WithLevel(NOTICE).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Warning(v ...any) {
+ l.WithLevel(WARNING).Print(v...)
+}
+
+func (l *Logger) Warningf(fmtstr string, v ...any) {
+ l.WithLevel(WARNING).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Warn(v ...any) {
+ l.WithLevel(WARNING).Print(v...)
+}
+
+func (l *Logger) Warnf(fmtstr string, v ...any) {
+ l.WithLevel(WARNING).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Error(v ...any) {
+ l.WithLevel(ERROR).Print(v...)
+}
+
+func (l *Logger) Errorf(fmtstr string, v ...any) {
+ l.WithLevel(ERROR).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Critical(v ...any) {
+ l.WithLevel(CRITICAL).Print(v...)
+}
+
+func (l *Logger) Criticalf(fmtstr string, v ...any) {
+ l.WithLevel(CRITICAL).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Alert(v ...any) {
+ l.WithLevel(ALERT).Print(v...)
+}
+
+func (l *Logger) Alertf(fmtstr string, v ...any) {
+ l.WithLevel(ALERT).Printf(fmtstr, v...)
+}
+
+func (l *Logger) Fatal(v ...any) {
+ if len(v) > 0 {
+ if arg1, ok := v[0].(string); ok {
+ if l.prefix != "" {
+ v[0] = FATAL.prefix(l.Writer()) + "[" + l.prefix + "] " + arg1
+ }
+ }
+ }
+
+ l.Logger.Fatal(v...)
+}
+
+func (l *Logger) Fatalf(fmtstr string, v ...any) {
+ if l.prefix != "" {
+ fmtstr = FATAL.prefix(l.Writer()) + "[" + l.prefix + "] " + fmtstr
+ }
+
+ l.Logger.Fatalf(fmtstr, v...)
+}
--- /dev/null
+package log
+
+func Redact(s string) string {
+ if len(s) < 2 {
+ return "*"
+ }
+
+ b := []byte(s)
+ for i := 1; i < len(b); i++ {
+ b[i] = '*'
+ }
+ return string(b)
+}
--- /dev/null
+package utils
+
+func Reverse[T any](s []T) []T {
+ for i := 0; i < len(s)/2; i++ {
+ j := len(s) - i - 1
+ s[i], s[j] = s[j], s[i]
+ }
+ return s
+}
--- /dev/null
+package stringmatch
+
+import (
+ "regexp"
+ "strings"
+)
+
+type StringMatcher interface {
+ Match(input string) bool
+}
+
+type Prefix string
+type Suffix string
+type Exact string
+type Contains string
+type Regexp string
+
+func (s Prefix) Match(input string) bool {
+ return strings.HasPrefix(input, string(s))
+}
+
+func (s Suffix) Match(input string) bool {
+ return strings.HasSuffix(input, string(s))
+}
+
+func (s Exact) Match(input string) bool {
+ return input == string(s)
+}
+
+func (s Contains) Match(input string) bool {
+ return strings.Contains(input, string(s))
+}
+
+func (s Regexp) Match(input string) bool {
+ re := regexp.MustCompile(string(s))
+ return re.MatchString(input)
+}
+
+type andMatcher struct {
+ matchers []StringMatcher
+}
+
+func (mm *andMatcher) Match(input string) bool {
+ for _, m := range mm.matchers {
+ if !m.Match(input) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func And(matchers ...StringMatcher) StringMatcher {
+ return &andMatcher{
+ matchers: matchers,
+ }
+}
+
+type orMatcher struct {
+ matchers []StringMatcher
+}
+
+func (mm *orMatcher) Match(input string) bool {
+ for _, m := range mm.matchers {
+ if m.Match(input) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func Or(matchers ...StringMatcher) StringMatcher {
+ return &orMatcher{
+ matchers: matchers,
+ }
+}