From: Dan Fuhry Date: Wed, 13 Dec 2023 03:48:13 +0000 (-0500) Subject: Initial commit X-Git-Url: https://go.fuhry.dev/?a=commitdiff_plain;h=733207b488cbdd95d71943ce263a75a3feab653b;p=runtime.git Initial commit --- 733207b488cbdd95d71943ce263a75a3feab653b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d81dcea --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +/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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1978c74 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +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) diff --git a/attestation/Makefile b/attestation/Makefile new file mode 100644 index 0000000..bdb19ef --- /dev/null +++ b/attestation/Makefile @@ -0,0 +1,58 @@ +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 diff --git a/attestation/c/config.m4 b/attestation/c/config.m4 new file mode 100644 index 0000000..7d02d27 --- /dev/null +++ b/attestation/c/config.m4 @@ -0,0 +1,5 @@ +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 diff --git a/attestation/c/main.c b/attestation/c/main.c new file mode 100644 index 0000000..1e996ea --- /dev/null +++ b/attestation/c/main.c @@ -0,0 +1,754 @@ +// include the PHP API itself +#include +#include + +#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); diff --git a/attestation/cgo/extension.h b/attestation/cgo/extension.h new file mode 100644 index 0000000..63ed2f2 --- /dev/null +++ b/attestation/cgo/extension.h @@ -0,0 +1,56 @@ +#ifndef TPM_ATTESTATION_EXTENSION_H +#define TPM_ATTESTATION_EXTENSION_H + +#include +#include + +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 */ diff --git a/attestation/cgo/extension_api.go b/attestation/cgo/extension_api.go new file mode 100644 index 0000000..9c7cd77 --- /dev/null +++ b/attestation/cgo/extension_api.go @@ -0,0 +1,168 @@ +package main + +// #include +// #include +// #include +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") +} diff --git a/attestation/client/Makefile b/attestation/client/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/attestation/client/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/attestation/client/attest.log b/attestation/client/attest.log new file mode 100644 index 0000000..9c445b3 --- /dev/null +++ b/attestation/client/attest.log @@ -0,0 +1,2 @@ +getting a new access token, because: access token has expired +fqWDVE7jfW3f7IqUsx9TgUC3lsAMFngn0JM1Em2SsSQ= diff --git a/attestation/client/main.go b/attestation/client/main.go new file mode 100644 index 0000000..18306b1 --- /dev/null +++ b/attestation/client/main.go @@ -0,0 +1,19 @@ +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) +} diff --git a/attestation/internal/attestation/api.go b/attestation/internal/attestation/api.go new file mode 100644 index 0000000..6b4d0de --- /dev/null +++ b/attestation/internal/attestation/api.go @@ -0,0 +1,33 @@ +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, + } +} diff --git a/attestation/internal/attestation/client.go b/attestation/internal/attestation/client.go new file mode 100644 index 0000000..7196d61 --- /dev/null +++ b/attestation/internal/attestation/client.go @@ -0,0 +1,195 @@ +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) + } +} diff --git a/attestation/internal/attestation/rpc_server.go b/attestation/internal/attestation/rpc_server.go new file mode 100644 index 0000000..62e6344 --- /dev/null +++ b/attestation/internal/attestation/rpc_server.go @@ -0,0 +1,206 @@ +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 +} diff --git a/attestation/internal/attestation/server.go b/attestation/internal/attestation/server.go new file mode 100644 index 0000000..2fcd2f5 --- /dev/null +++ b/attestation/internal/attestation/server.go @@ -0,0 +1,223 @@ +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 +} diff --git a/attestation/php.ini.in b/attestation/php.ini.in new file mode 100644 index 0000000..3494c76 --- /dev/null +++ b/attestation/php.ini.in @@ -0,0 +1 @@ +extension=@PWD@/c/modules/tpm_attestation.so diff --git a/attestation/php/composer.json b/attestation/php/composer.json new file mode 100644 index 0000000..d7a46de --- /dev/null +++ b/attestation/php/composer.json @@ -0,0 +1,20 @@ +{ + "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" + } + ] +} diff --git a/attestation/php/composer.lock b/attestation/php/composer.lock new file mode 100644 index 0000000..537380f --- /dev/null +++ b/attestation/php/composer.lock @@ -0,0 +1,1749 @@ +{ + "_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" +} diff --git a/attestation/php/phpunit.xml b/attestation/php/phpunit.xml new file mode 100644 index 0000000..9b62e56 --- /dev/null +++ b/attestation/php/phpunit.xml @@ -0,0 +1,8 @@ + + + + + ./tests + + + diff --git a/attestation/php/src/ActivationChallenge.php b/attestation/php/src/ActivationChallenge.php new file mode 100644 index 0000000..72023b1 --- /dev/null +++ b/attestation/php/src/ActivationChallenge.php @@ -0,0 +1,31 @@ +Secret; + } + + public function toJson(): array + { + throw new \RuntimeException("tpm_attestation PHP extension is not loaded"); + + return [ + 'EncryptedSecret' => $this->EncryptedSecret, + 'Credential' => $this->Credential, + ]; + } +} diff --git a/attestation/php/src/AttestationParameters.php b/attestation/php/src/AttestationParameters.php new file mode 100644 index 0000000..e234626 --- /dev/null +++ b/attestation/php/src/AttestationParameters.php @@ -0,0 +1,66 @@ +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"); + } +} diff --git a/attestation/php/src/PlatformParameters.php b/attestation/php/src/PlatformParameters.php new file mode 100644 index 0000000..d21d06e --- /dev/null +++ b/attestation/php/src/PlatformParameters.php @@ -0,0 +1,26 @@ + "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", + ); + } +} diff --git a/attestation/php/tests/AttestPlatformTest.php b/attestation/php/tests/AttestPlatformTest.php new file mode 100644 index 0000000..d6db938 --- /dev/null +++ b/attestation/php/tests/AttestPlatformTest.php @@ -0,0 +1,174 @@ +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" + ]; +} diff --git a/attestation/rpc_client/Makefile b/attestation/rpc_client/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/attestation/rpc_client/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/attestation/rpc_client/main.go b/attestation/rpc_client/main.go new file mode 100644 index 0000000..aad5985 --- /dev/null +++ b/attestation/rpc_client/main.go @@ -0,0 +1,42 @@ +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) +} diff --git a/attestation/rpc_server/Makefile b/attestation/rpc_server/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/attestation/rpc_server/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/attestation/rpc_server/attest_acl.yaml b/attestation/rpc_server/attest_acl.yaml new file mode 100644 index 0000000..582eda6 --- /dev/null +++ b/attestation/rpc_server/attest_acl.yaml @@ -0,0 +1,3 @@ +DEFAULT: + - service: attest + - user: '*' diff --git a/attestation/rpc_server/main.go b/attestation/rpc_server/main.go new file mode 100644 index 0000000..5945a5a --- /dev/null +++ b/attestation/rpc_server/main.go @@ -0,0 +1,43 @@ +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") +} diff --git a/constants/constants.go b/constants/constants.go new file mode 100644 index 0000000..05d1e7f --- /dev/null +++ b/constants/constants.go @@ -0,0 +1,15 @@ +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" +) diff --git a/echo/client/Makefile b/echo/client/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/echo/client/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/echo/client/main.go b/echo/client/main.go new file mode 100644 index 0000000..98063ec --- /dev/null +++ b/echo/client/main.go @@ -0,0 +1,52 @@ +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) +} diff --git a/echo/server.go b/echo/server.go new file mode 100644 index 0000000..bafcf50 --- /dev/null +++ b/echo/server.go @@ -0,0 +1,45 @@ +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 +} diff --git a/echo/server/Makefile b/echo/server/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/echo/server/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/echo/server/echo_acl.yaml b/echo/server/echo_acl.yaml new file mode 100644 index 0000000..4be2982 --- /dev/null +++ b/echo/server/echo_acl.yaml @@ -0,0 +1,10 @@ +DEFAULT: + - service: echo + +fuhry.runtime.service.echo.Echo/Echo: + - user: '*' + - service: echo + +fuhry.runtime.service.echo.Echo/Greet: + - service: '*' + - user: '*' diff --git a/echo/server/main.go b/echo/server/main.go new file mode 100644 index 0000000..a4b30fd --- /dev/null +++ b/echo/server/main.go @@ -0,0 +1,43 @@ +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") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..46e2c5c --- /dev/null +++ b/go.mod @@ -0,0 +1,108 @@ +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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c156bf5 --- /dev/null +++ b/go.sum @@ -0,0 +1,998 @@ +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= diff --git a/grpc/acl/acl_yaml.go b/grpc/acl/acl_yaml.go new file mode 100644 index 0000000..53ed632 --- /dev/null +++ b/grpc/acl/acl_yaml.go @@ -0,0 +1,143 @@ +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) +} diff --git a/grpc/client.go b/grpc/client.go new file mode 100644 index 0000000..e904b30 --- /dev/null +++ b/grpc/client.go @@ -0,0 +1,74 @@ +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 +} diff --git a/grpc/conn_base.go b/grpc/conn_base.go new file mode 100644 index 0000000..ef76db6 --- /dev/null +++ b/grpc/conn_base.go @@ -0,0 +1,53 @@ +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 +} diff --git a/grpc/conn_quic.go b/grpc/conn_quic.go new file mode 100644 index 0000000..99d16df --- /dev/null +++ b/grpc/conn_quic.go @@ -0,0 +1,43 @@ +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{} }) +} diff --git a/grpc/conn_tcp.go b/grpc/conn_tcp.go new file mode 100644 index 0000000..048b26a --- /dev/null +++ b/grpc/conn_tcp.go @@ -0,0 +1,54 @@ +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{} }) +} diff --git a/grpc/context.go b/grpc/context.go new file mode 100644 index 0000000..897e345 --- /dev/null +++ b/grpc/context.go @@ -0,0 +1,58 @@ +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 +} diff --git a/grpc/server.go b/grpc/server.go new file mode 100644 index 0000000..94b74be --- /dev/null +++ b/grpc/server.go @@ -0,0 +1,220 @@ +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") +} diff --git a/ldap/health_exporter/Makefile b/ldap/health_exporter/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/ldap/health_exporter/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/ldap/health_exporter/main.go b/ldap/health_exporter/main.go new file mode 100644 index 0000000..4d3da0f --- /dev/null +++ b/ldap/health_exporter/main.go @@ -0,0 +1,177 @@ +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() +} diff --git a/ldap/health_exporter/systemd/ldap-health-exporter@.service b/ldap/health_exporter/systemd/ldap-health-exporter@.service new file mode 100644 index 0000000..78dfc55 --- /dev/null +++ b/ldap/health_exporter/systemd/ldap-health-exporter@.service @@ -0,0 +1,14 @@ +[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 + diff --git a/ldap/scraper.go b/ldap/scraper.go new file mode 100644 index 0000000..bdad83f --- /dev/null +++ b/ldap/scraper.go @@ -0,0 +1,346 @@ +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 +} diff --git a/ldap/server.go b/ldap/server.go new file mode 100644 index 0000000..1b8c467 --- /dev/null +++ b/ldap/server.go @@ -0,0 +1,103 @@ +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 +} diff --git a/machines/client.go b/machines/client.go new file mode 100644 index 0000000..baffa99 --- /dev/null +++ b/machines/client.go @@ -0,0 +1,149 @@ +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) +} diff --git a/machines/event_monitor/Makefile b/machines/event_monitor/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/machines/event_monitor/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/machines/event_monitor/main.go b/machines/event_monitor/main.go new file mode 100644 index 0000000..c79325a --- /dev/null +++ b/machines/event_monitor/main.go @@ -0,0 +1,45 @@ +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) +} diff --git a/machines/event_watcher.go b/machines/event_watcher.go new file mode 100644 index 0000000..8a5db9e --- /dev/null +++ b/machines/event_watcher.go @@ -0,0 +1,111 @@ +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 +} diff --git a/machines/oauth2.go b/machines/oauth2.go new file mode 100644 index 0000000..bdd740b --- /dev/null +++ b/machines/oauth2.go @@ -0,0 +1,223 @@ +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 +} diff --git a/machines/types.go b/machines/types.go new file mode 100644 index 0000000..8402d03 --- /dev/null +++ b/machines/types.go @@ -0,0 +1,143 @@ +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 +} diff --git a/metrics/apcups_exporter/Makefile b/metrics/apcups_exporter/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/metrics/apcups_exporter/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/metrics/apcups_exporter/main.go b/metrics/apcups_exporter/main.go new file mode 100644 index 0000000..0fa6b6e --- /dev/null +++ b/metrics/apcups_exporter/main.go @@ -0,0 +1,77 @@ +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 + } + } +} diff --git a/metrics/metricbus/PROTOCOL.md b/metrics/metricbus/PROTOCOL.md new file mode 100644 index 0000000..ed155c6 --- /dev/null +++ b/metrics/metricbus/PROTOCOL.md @@ -0,0 +1,58 @@ +# 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. diff --git a/metrics/metricbus/constants.go b/metrics/metricbus/constants.go new file mode 100644 index 0000000..3bdf05e --- /dev/null +++ b/metrics/metricbus/constants.go @@ -0,0 +1,91 @@ +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 +} diff --git a/metrics/metricbus/dbus-1/dev.fuhry.runtime.metrics.conf b/metrics/metricbus/dbus-1/dev.fuhry.runtime.metrics.conf new file mode 100644 index 0000000..8978141 --- /dev/null +++ b/metrics/metricbus/dbus-1/dev.fuhry.runtime.metrics.conf @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/metrics/metricbus/internal/dbus_interface.go b/metrics/metricbus/internal/dbus_interface.go new file mode 100644 index 0000000..d5fffd0 --- /dev/null +++ b/metrics/metricbus/internal/dbus_interface.go @@ -0,0 +1,52 @@ +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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + ` + introspect.IntrospectDataString + ` + +` diff --git a/metrics/metricbus/internal/server.go b/metrics/metricbus/internal/server.go new file mode 100644 index 0000000..8c51d60 --- /dev/null +++ b/metrics/metricbus/internal/server.go @@ -0,0 +1,515 @@ +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) +} diff --git a/metrics/metricbus/mbclient/client.go b/metrics/metricbus/mbclient/client.go new file mode 100644 index 0000000..d2a1145 --- /dev/null +++ b/metrics/metricbus/mbclient/client.go @@ -0,0 +1,269 @@ +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] +} diff --git a/metrics/metricbus/mbclient/conn.go b/metrics/metricbus/mbclient/conn.go new file mode 100644 index 0000000..80e8e93 --- /dev/null +++ b/metrics/metricbus/mbclient/conn.go @@ -0,0 +1,167 @@ +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 +} diff --git a/metrics/metricbus/mbclient/example/Makefile b/metrics/metricbus/mbclient/example/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/metrics/metricbus/mbclient/example/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/metrics/metricbus/mbclient/example/main.go b/metrics/metricbus/mbclient/example/main.go new file mode 100644 index 0000000..9f9a72f --- /dev/null +++ b/metrics/metricbus/mbclient/example/main.go @@ -0,0 +1,43 @@ +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 + } + } +} diff --git a/metrics/metricbus/mbclient/intf.go b/metrics/metricbus/mbclient/intf.go new file mode 100644 index 0000000..8b5ccdb --- /dev/null +++ b/metrics/metricbus/mbclient/intf.go @@ -0,0 +1,34 @@ +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() +} diff --git a/metrics/metricbus/mbclient/metrics.go b/metrics/metricbus/mbclient/metrics.go new file mode 100644 index 0000000..45bd0c5 --- /dev/null +++ b/metrics/metricbus/mbclient/metrics.go @@ -0,0 +1,117 @@ +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 +} diff --git a/metrics/metricbus/mbserver/Makefile b/metrics/metricbus/mbserver/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/metrics/metricbus/mbserver/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/metrics/metricbus/mbserver/main.go b/metrics/metricbus/mbserver/main.go new file mode 100644 index 0000000..237116c --- /dev/null +++ b/metrics/metricbus/mbserver/main.go @@ -0,0 +1,31 @@ +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() +} diff --git a/metrics/metricbus/systemd/system/metric-collector.service b/metrics/metricbus/systemd/system/metric-collector.service new file mode 100644 index 0000000..caff109 --- /dev/null +++ b/metrics/metricbus/systemd/system/metric-collector.service @@ -0,0 +1,12 @@ +[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 diff --git a/mtls/certutil/certutil.go b/mtls/certutil/certutil.go new file mode 100644 index 0000000..3e1bc99 --- /dev/null +++ b/mtls/certutil/certutil.go @@ -0,0 +1,155 @@ +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) +} diff --git a/mtls/fsnotify/fsnotify.go b/mtls/fsnotify/fsnotify.go new file mode 100644 index 0000000..367906c --- /dev/null +++ b/mtls/fsnotify/fsnotify.go @@ -0,0 +1,142 @@ +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 +} diff --git a/mtls/identity.go b/mtls/identity.go new file mode 100644 index 0000000..451cd60 --- /dev/null +++ b/mtls/identity.go @@ -0,0 +1,358 @@ +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") +} diff --git a/mtls/pkcs11.go b/mtls/pkcs11.go new file mode 100644 index 0000000..285367a --- /dev/null +++ b/mtls/pkcs11.go @@ -0,0 +1,100 @@ +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 +} diff --git a/mtls/provider_file.go b/mtls/provider_file.go new file mode 100644 index 0000000..b8abba9 --- /dev/null +++ b/mtls/provider_file.go @@ -0,0 +1,508 @@ +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 + }) +} diff --git a/mtls/provider_interface.go b/mtls/provider_interface.go new file mode 100644 index 0000000..f42f122 --- /dev/null +++ b/mtls/provider_interface.go @@ -0,0 +1,66 @@ +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) +} diff --git a/mtls/provider_keychain_macos.go b/mtls/provider_keychain_macos.go new file mode 100644 index 0000000..104f642 --- /dev/null +++ b/mtls/provider_keychain_macos.go @@ -0,0 +1,458 @@ +//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 + }) +} diff --git a/mtls/provider_shared.go b/mtls/provider_shared.go new file mode 100644 index 0000000..183e017 --- /dev/null +++ b/mtls/provider_shared.go @@ -0,0 +1,46 @@ +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 +} diff --git a/mtls/provider_tpm2_pkcs11.go b/mtls/provider_tpm2_pkcs11.go new file mode 100644 index 0000000..cbec7a8 --- /dev/null +++ b/mtls/provider_tpm2_pkcs11.go @@ -0,0 +1,115 @@ +//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() + }) +} diff --git a/mtls/verify_names.go b/mtls/verify_names.go new file mode 100644 index 0000000..d1d180b --- /dev/null +++ b/mtls/verify_names.go @@ -0,0 +1,269 @@ +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[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/service/(?P[a-z0-9_-]+)$` + exprSpiffeUserIdentity = `^spiffe://(?P[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/user/(?P[a-z0-9_-]+)$` + exprMtlsInternalIdenitty = `^(?P[A-Za-z0-9_-]+)\.(?P[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) +} diff --git a/mtls/verify_roots.go b/mtls/verify_roots.go new file mode 100644 index 0000000..1a95101 --- /dev/null +++ b/mtls/verify_roots.go @@ -0,0 +1,207 @@ +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) + } +} diff --git a/mtls/verify_tool/Makefile b/mtls/verify_tool/Makefile new file mode 100644 index 0000000..1d7d424 --- /dev/null +++ b/mtls/verify_tool/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/mtls/verify_tool/main.go b/mtls/verify_tool/main.go new file mode 100644 index 0000000..26f4c27 --- /dev/null +++ b/mtls/verify_tool/main.go @@ -0,0 +1,89 @@ +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) +} diff --git a/net/dns/dns_cache.go b/net/dns/dns_cache.go new file mode 100644 index 0000000..4a3a6fa --- /dev/null +++ b/net/dns/dns_cache.go @@ -0,0 +1,145 @@ +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, + }, + }, + } +} diff --git a/proto/service/attest/Makefile b/proto/service/attest/Makefile new file mode 100644 index 0000000..501225c --- /dev/null +++ b/proto/service/attest/Makefile @@ -0,0 +1,14 @@ +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) diff --git a/proto/service/attest/attest_server.pb.go b/proto/service/attest/attest_server.pb.go new file mode 100644 index 0000000..276a999 --- /dev/null +++ b/proto/service/attest/attest_server.pb.go @@ -0,0 +1,853 @@ +// 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 +} diff --git a/proto/service/attest/attest_server.proto b/proto/service/attest/attest_server.proto new file mode 100644 index 0000000..4557321 --- /dev/null +++ b/proto/service/attest/attest_server.proto @@ -0,0 +1,67 @@ +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; +} diff --git a/proto/service/attest/attest_server_grpc.pb.go b/proto/service/attest/attest_server_grpc.pb.go new file mode 100644 index 0000000..7f41c29 --- /dev/null +++ b/proto/service/attest/attest_server_grpc.pb.go @@ -0,0 +1,146 @@ +// 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", +} diff --git a/proto/service/attest/convert.go b/proto/service/attest/convert.go new file mode 100644 index 0000000..7e63cb9 --- /dev/null +++ b/proto/service/attest/convert.go @@ -0,0 +1,142 @@ +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, + } +} diff --git a/proto/service/echo/Makefile b/proto/service/echo/Makefile new file mode 100644 index 0000000..cc9ec69 --- /dev/null +++ b/proto/service/echo/Makefile @@ -0,0 +1,11 @@ +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 diff --git a/proto/service/echo/echo_server.pb.go b/proto/service/echo/echo_server.pb.go new file mode 100644 index 0000000..17772bd --- /dev/null +++ b/proto/service/echo/echo_server.pb.go @@ -0,0 +1,338 @@ +// 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 +} diff --git a/proto/service/echo/echo_server.proto b/proto/service/echo/echo_server.proto new file mode 100644 index 0000000..5b9b015 --- /dev/null +++ b/proto/service/echo/echo_server.proto @@ -0,0 +1,24 @@ +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; +} diff --git a/proto/service/echo/echo_server_grpc.pb.go b/proto/service/echo/echo_server_grpc.pb.go new file mode 100644 index 0000000..e606890 --- /dev/null +++ b/proto/service/echo/echo_server_grpc.pb.go @@ -0,0 +1,146 @@ +// 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", +} diff --git a/rand/range.go b/rand/range.go new file mode 100644 index 0000000..e750f7a --- /dev/null +++ b/rand/range.go @@ -0,0 +1,15 @@ +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 +} diff --git a/sase/acl.go b/sase/acl.go new file mode 100644 index 0000000..dbefd12 --- /dev/null +++ b/sase/acl.go @@ -0,0 +1,125 @@ +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 +} diff --git a/sase/acl_test.go b/sase/acl_test.go new file mode 100644 index 0000000..ff0ab75 --- /dev/null +++ b/sase/acl_test.go @@ -0,0 +1,59 @@ +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) + } +} diff --git a/sase/client.go b/sase/client.go new file mode 100644 index 0000000..0ed4f39 --- /dev/null +++ b/sase/client.go @@ -0,0 +1,51 @@ +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) +} diff --git a/sase/happy_eyeballs.go b/sase/happy_eyeballs.go new file mode 100644 index 0000000..9eb789a --- /dev/null +++ b/sase/happy_eyeballs.go @@ -0,0 +1,64 @@ +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 + } + } +} diff --git a/sase/machines_networks.go b/sase/machines_networks.go new file mode 100644 index 0000000..9baa5f8 --- /dev/null +++ b/sase/machines_networks.go @@ -0,0 +1,100 @@ +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 +} diff --git a/sase/read_writer.go b/sase/read_writer.go new file mode 100644 index 0000000..7d2195b --- /dev/null +++ b/sase/read_writer.go @@ -0,0 +1,85 @@ +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 +} diff --git a/sase/ws_proxy.go b/sase/ws_proxy.go new file mode 100644 index 0000000..f0c4566 --- /dev/null +++ b/sase/ws_proxy.go @@ -0,0 +1,238 @@ +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[a-z]{1,8})/(?P[a-z0-9-]+(?:\.[a-z0-9-]+)*)/(?P[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() +} diff --git a/sase/ws_proxy_client/Makefile b/sase/ws_proxy_client/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sase/ws_proxy_client/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sase/ws_proxy_client/main.go b/sase/ws_proxy_client/main.go new file mode 100644 index 0000000..69bb6b1 --- /dev/null +++ b/sase/ws_proxy_client/main.go @@ -0,0 +1,63 @@ +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() +} diff --git a/sase/ws_tcp_proxy/Makefile b/sase/ws_tcp_proxy/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sase/ws_tcp_proxy/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sase/ws_tcp_proxy/main.go b/sase/ws_tcp_proxy/main.go new file mode 100644 index 0000000..89e5057 --- /dev/null +++ b/sase/ws_tcp_proxy/main.go @@ -0,0 +1,89 @@ +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() +} diff --git a/sd/etcd_factory.go b/sd/etcd_factory.go new file mode 100644 index 0000000..f264ed4 --- /dev/null +++ b/sd/etcd_factory.go @@ -0,0 +1,100 @@ +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") +} diff --git a/sd/health_exporter/Makefile b/sd/health_exporter/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sd/health_exporter/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sd/health_exporter/main.go b/sd/health_exporter/main.go new file mode 100644 index 0000000..6b7c87e --- /dev/null +++ b/sd/health_exporter/main.go @@ -0,0 +1,148 @@ +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 +} diff --git a/sd/healthcheck.go b/sd/healthcheck.go new file mode 100644 index 0000000..fd87d10 --- /dev/null +++ b/sd/healthcheck.go @@ -0,0 +1,280 @@ +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 +} diff --git a/sd/healthcheck_http.go b/sd/healthcheck_http.go new file mode 100644 index 0000000..00a54d9 --- /dev/null +++ b/sd/healthcheck_http.go @@ -0,0 +1,147 @@ +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) +} diff --git a/sd/healthcheck_ldap.go b/sd/healthcheck_ldap.go new file mode 100644 index 0000000..5b54ae0 --- /dev/null +++ b/sd/healthcheck_ldap.go @@ -0,0 +1,143 @@ +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) +} diff --git a/sd/monitor.go b/sd/monitor.go new file mode 100644 index 0000000..a50fca8 --- /dev/null +++ b/sd/monitor.go @@ -0,0 +1,145 @@ +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 +} diff --git a/sd/publish.go b/sd/publish.go new file mode 100644 index 0000000..d2af708 --- /dev/null +++ b/sd/publish.go @@ -0,0 +1,338 @@ +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) +} diff --git a/sd/sd_publish/Makefile b/sd/sd_publish/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sd/sd_publish/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sd/sd_publish/main.go b/sd/sd_publish/main.go new file mode 100644 index 0000000..76813c9 --- /dev/null +++ b/sd/sd_publish/main.go @@ -0,0 +1,47 @@ +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() +} diff --git a/sd/sd_register/Makefile b/sd/sd_register/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sd/sd_register/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sd/sd_register/main.go b/sd/sd_register/main.go new file mode 100644 index 0000000..2f85d09 --- /dev/null +++ b/sd/sd_register/main.go @@ -0,0 +1,121 @@ +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) +} diff --git a/sd/sd_watcher/Makefile b/sd/sd_watcher/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/sd/sd_watcher/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/sd/sd_watcher/main.go b/sd/sd_watcher/main.go new file mode 100644 index 0000000..a88a793 --- /dev/null +++ b/sd/sd_watcher/main.go @@ -0,0 +1,70 @@ +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 + } + } +} diff --git a/sd/systemd/sd-health-exporter.service b/sd/systemd/sd-health-exporter.service new file mode 100644 index 0000000..81f2680 --- /dev/null +++ b/sd/systemd/sd-health-exporter.service @@ -0,0 +1,14 @@ +[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 diff --git a/sd/systemd/sd-register.service b/sd/systemd/sd-register.service new file mode 100644 index 0000000..fe525ec --- /dev/null +++ b/sd/systemd/sd-register.service @@ -0,0 +1,14 @@ +[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 + diff --git a/sd/systemd/sd-register@.service b/sd/systemd/sd-register@.service new file mode 100644 index 0000000..79ff522 --- /dev/null +++ b/sd/systemd/sd-register@.service @@ -0,0 +1,14 @@ +[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 + diff --git a/sd/watcher.go b/sd/watcher.go new file mode 100644 index 0000000..1e334c7 --- /dev/null +++ b/sd/watcher.go @@ -0,0 +1,225 @@ +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()), + } +} diff --git a/thirdparty/registry/Makefile b/thirdparty/registry/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/thirdparty/registry/Makefile @@ -0,0 +1,14 @@ +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 $@ $< + diff --git a/thirdparty/registry/config.yml b/thirdparty/registry/config.yml new file mode 100644 index 0000000..ff32304 --- /dev/null +++ b/thirdparty/registry/config.yml @@ -0,0 +1,21 @@ +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 + diff --git a/thirdparty/registry/main.go b/thirdparty/registry/main.go new file mode 100644 index 0000000..592e6a6 --- /dev/null +++ b/thirdparty/registry/main.go @@ -0,0 +1,218 @@ +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) + }) +} diff --git a/thirdparty/registry/systemd/docker-registry.service b/thirdparty/registry/systemd/docker-registry.service new file mode 100644 index 0000000..430b9fb --- /dev/null +++ b/thirdparty/registry/systemd/docker-registry.service @@ -0,0 +1,14 @@ +[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 + diff --git a/utils/ansi/color.go b/utils/ansi/color.go new file mode 100644 index 0000000..a4dd395 --- /dev/null +++ b/utils/ansi/color.go @@ -0,0 +1,44 @@ +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 +} diff --git a/utils/debounce/debounce.go b/utils/debounce/debounce.go new file mode 100644 index 0000000..c620a49 --- /dev/null +++ b/utils/debounce/debounce.go @@ -0,0 +1,47 @@ +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() + } +} diff --git a/utils/debounce/debounce_test.go b/utils/debounce/debounce_test.go new file mode 100644 index 0000000..8a249b3 --- /dev/null +++ b/utils/debounce/debounce_test.go @@ -0,0 +1,75 @@ +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)) +} diff --git a/utils/generics/math.go b/utils/generics/math.go new file mode 100644 index 0000000..5d73e06 --- /dev/null +++ b/utils/generics/math.go @@ -0,0 +1,36 @@ +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 +} diff --git a/utils/hashset/hashset.go b/utils/hashset/hashset.go new file mode 100644 index 0000000..7509728 --- /dev/null +++ b/utils/hashset/hashset.go @@ -0,0 +1,165 @@ +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()) +} diff --git a/utils/hashset/hashset_test.go b/utils/hashset/hashset_test.go new file mode 100644 index 0000000..9334672 --- /dev/null +++ b/utils/hashset/hashset_test.go @@ -0,0 +1,103 @@ +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) +} diff --git a/utils/hostname/hostname.go b/utils/hostname/hostname.go new file mode 100644 index 0000000..f99e32d --- /dev/null +++ b/utils/hostname/hostname.go @@ -0,0 +1,83 @@ +//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) +} diff --git a/utils/hostname/hostname_macos.go b/utils/hostname/hostname_macos.go new file mode 100644 index 0000000..75e07da --- /dev/null +++ b/utils/hostname/hostname_macos.go @@ -0,0 +1,144 @@ +//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\"") +} diff --git a/utils/log/level.go b/utils/log/level.go new file mode 100644 index 0000000..d25c8c1 --- /dev/null +++ b/utils/log/level.go @@ -0,0 +1,119 @@ +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 + } +} diff --git a/utils/log/log.go b/utils/log/log.go new file mode 100644 index 0000000..843118a --- /dev/null +++ b/utils/log/log.go @@ -0,0 +1,223 @@ +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...) +} diff --git a/utils/log/util.go b/utils/log/util.go new file mode 100644 index 0000000..614dd97 --- /dev/null +++ b/utils/log/util.go @@ -0,0 +1,13 @@ +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) +} diff --git a/utils/reverse.go b/utils/reverse.go new file mode 100644 index 0000000..d5994b6 --- /dev/null +++ b/utils/reverse.go @@ -0,0 +1,9 @@ +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 +} diff --git a/utils/stringmatch/stringmatch.go b/utils/stringmatch/stringmatch.go new file mode 100644 index 0000000..3a7a83f --- /dev/null +++ b/utils/stringmatch/stringmatch.go @@ -0,0 +1,77 @@ +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, + } +}