diff --git a/cmd/gcp/main.go b/cmd/gcp/main.go index d3b04dd5..e7f8b5d8 100644 --- a/cmd/gcp/main.go +++ b/cmd/gcp/main.go @@ -27,7 +27,6 @@ import ( "syscall" "time" - "github.com/google/trillian/crypto/keys/pem" "github.com/google/trillian/monitoring/opencensus" "github.com/google/trillian/monitoring/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -50,25 +49,25 @@ var ( notAfterStart timestampFlag notAfterLimit timestampFlag - httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).") - metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.") - tesseraDeadline = flag.Duration("tessera_deadline", time.Second*10, "Deadline for Tessera requests.") - maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.") - tracing = flag.Bool("tracing", false, "If true opencensus Stackdriver tracing will be enabled. See https://opencensus.io/.") - tracingProjectID = flag.String("tracing_project_id", "", "project ID to pass to stackdriver. Can be empty for GCP, consult docs for other platforms.") - tracingPercent = flag.Int("tracing_percent", 0, "Percent of requests to be traced. Zero is a special case to use the DefaultSampler.") - origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") - projectID = flag.String("project_id", "", "GCP ProjectID.") - bucket = flag.String("bucket", "", "Name of the bucket to store the log in.") - spannerDB = flag.String("spanner_db_path", "", "Spanner database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") - spannerDedupDB = flag.String("spanner_dedup_db_path", "", "Spanner deduplication database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") - rootsPemFile = flag.String("roots_pem_file", "", "Path to the file containing root certificates that are acceptable to the log. The certs are served through get-roots endpoint.") - rejectExpired = flag.Bool("reject_expired", false, "If true then the certificate validity period will be checked against the current time during the validation of submissions. This will cause expired certificates to be rejected.") - rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.") - extKeyUsages = flag.String("ext_key_usages", "", "If set, will restrict the set of such usages that the server will accept. By default all are accepted. The values specified must be ones known to the x509 package.") - rejectExtensions = flag.String("reject_extension", "", "A list of X.509 extension OIDs, in dotted string form (e.g. '2.3.4.5') which, if present, should cause submissions to be rejected.") - privKey = flag.String("private_key", "", "Path to a private key .der file. Used to sign checkpoints and SCTs.") - privKeyPass = flag.String("password", "", "private_key password.") + httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port).") + metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint.") + tesseraDeadline = flag.Duration("tessera_deadline", time.Second*10, "Deadline for Tessera requests.") + maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses.") + tracing = flag.Bool("tracing", false, "If true opencensus Stackdriver tracing will be enabled. See https://opencensus.io/.") + tracingProjectID = flag.String("tracing_project_id", "", "project ID to pass to stackdriver. Can be empty for GCP, consult docs for other platforms.") + tracingPercent = flag.Int("tracing_percent", 0, "Percent of requests to be traced. Zero is a special case to use the DefaultSampler.") + origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") + projectID = flag.String("project_id", "", "GCP ProjectID.") + bucket = flag.String("bucket", "", "Name of the bucket to store the log in.") + spannerDB = flag.String("spanner_db_path", "", "Spanner database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") + spannerDedupDB = flag.String("spanner_dedup_db_path", "", "Spanner deduplication database path: projects/{projectId}/instances/{instanceId}/databases/{databaseId}.") + rootsPemFile = flag.String("roots_pem_file", "", "Path to the file containing root certificates that are acceptable to the log. The certs are served through get-roots endpoint.") + rejectExpired = flag.Bool("reject_expired", false, "If true then the certificate validity period will be checked against the current time during the validation of submissions. This will cause expired certificates to be rejected.") + rejectUnexpired = flag.Bool("reject_unexpired", false, "If true then CTFE rejects certificates that are either currently valid or not yet valid.") + extKeyUsages = flag.String("ext_key_usages", "", "If set, will restrict the set of such usages that the server will accept. By default all are accepted. The values specified must be ones known to the x509 package.") + rejectExtensions = flag.String("reject_extension", "", "A list of X.509 extension OIDs, in dotted string form (e.g. '2.3.4.5') which, if present, should cause submissions to be rejected.") + signerPublicKeySecretName = flag.String("signer_public_key_secret_name", "", "Public key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") + signerPrivateKeySecretName = flag.String("signer_private_key_secret_name", "", "Private key secret name for checkpoints and SCTs signer. Format: projects/{projectId}/secrets/{secretName}/versions/{secretVersion}.") ) // nolint:staticcheck @@ -77,10 +76,9 @@ func main() { flag.Parse() ctx := context.Background() - // TODO(phboneff): move to something else, like KMS - signer, err := pem.ReadPrivateKeyFile(*privKey, *privKeyPass) + signer, err := NewSecretManagerSigner(ctx, *signerPublicKeySecretName, *signerPrivateKeySecretName) if err != nil { - klog.Exitf("Can't open key: %v", err) + klog.Exitf("Can't create secret manager signer: %v", err) } vCfg, err := sctfe.ValidateLogConfig(*origin, *projectID, *bucket, *spannerDB, *rootsPemFile, *rejectExpired, *rejectUnexpired, *extKeyUsages, *rejectExtensions, notAfterStart.t, notAfterLimit.t, signer) diff --git a/cmd/gcp/secret_manager.go b/cmd/gcp/secret_manager.go new file mode 100644 index 00000000..e916161b --- /dev/null +++ b/cmd/gcp/secret_manager.go @@ -0,0 +1,143 @@ +// Copyright 2024 Google LLC. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "hash/crc32" + "io" + + secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" +) + +// ECDSAWithSHA256Signer implements crypto.Signer using Google Cloud Secret Manager. +// Only crypto.SHA256 and ECDSA are supported. +type ECDSAWithSHA256Signer struct { + publicKey *ecdsa.PublicKey + privateKey *ecdsa.PrivateKey +} + +// Public returns the public key stored in the Signer object. +func (s *ECDSAWithSHA256Signer) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key stored in Google Cloud Secret Manager. +func (s *ECDSAWithSHA256Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // Verify hash function and digest bytes length. + if opts == nil { + return nil, errors.New("opts cannot be nil") + } + if opts.HashFunc() != crypto.SHA256 { + return nil, fmt.Errorf("unsupported hash func: %v", opts.HashFunc()) + } + if len(digest) != opts.HashFunc().Size() { + return nil, fmt.Errorf("digest bytes length %d does not match hash function bytes length %d", len(digest), opts.HashFunc().Size()) + } + + return ecdsa.SignASN1(rand, s.privateKey, digest) +} + +// NewSecretManagerSigner creates a new signer that uses the ECDSA P-256 key pair in +// Google Cloud Secret Manager for signing digests. +func NewSecretManagerSigner(ctx context.Context, publicKeySecretName, privateKeySecretName string) (*ECDSAWithSHA256Signer, error) { + client, err := secretmanager.NewClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create secret manager client: %w", err) + } + defer client.Close() + + // Public Key + var publicKey crypto.PublicKey + pemBlock, err := secretPEM(ctx, client, publicKeySecretName) + if err != nil { + return nil, fmt.Errorf("failed to get public key secret PEM (%s): %w", publicKeySecretName, err) + } + switch pemBlock.Type { + case "PUBLIC KEY": + publicKey, err = x509.ParsePKIXPublicKey(pemBlock.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type) + } + if err != nil { + return nil, err + } + var ecdsaPublicKey *ecdsa.PublicKey + ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("the public key stored in Secret Manager is not an ECDSA key") + } + + // Private Key + var ecdsaPrivateKey *ecdsa.PrivateKey + pemBlock, err = secretPEM(ctx, client, privateKeySecretName) + if err != nil { + return nil, fmt.Errorf("failed to get private key secret PEM (%s): %w", privateKeySecretName, err) + } + switch pemBlock.Type { + case "EC PRIVATE KEY": + ecdsaPrivateKey, err = x509.ParseECPrivateKey(pemBlock.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM type: %s", pemBlock.Type) + } + if err != nil { + return nil, err + } + + // Verify the correctness of the signer key pair + if !ecdsaPrivateKey.PublicKey.Equal(ecdsaPublicKey) { + return nil, errors.New("signer key pair doesn't match") + } + + return &ECDSAWithSHA256Signer{ + publicKey: ecdsaPublicKey, + privateKey: ecdsaPrivateKey, + }, nil +} + +func secretPEM(ctx context.Context, client *secretmanager.Client, secretName string) (*pem.Block, error) { + resp, err := client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ + Name: secretName, + }) + if err != nil { + return nil, fmt.Errorf("failed to access secret version: %w", err) + } + if resp.Name != secretName { + return nil, errors.New("request corrupted in-transit") + } + // Verify the data checksum. + crc32c := crc32.MakeTable(crc32.Castagnoli) + checksum := int64(crc32.Checksum(resp.Payload.Data, crc32c)) + if checksum != *resp.Payload.DataCrc32C { + return nil, errors.New("Data corruption detected.") + } + + pemBlock, rest := pem.Decode([]byte(resp.Payload.Data)) + if pemBlock == nil { + return nil, errors.New("failed to decode PEM") + } + if len(rest) > 0 { + return nil, fmt.Errorf("extra data after decoding PEM: %v", rest) + } + + return pemBlock, nil +} diff --git a/config.go b/config.go index 8e524c5b..5889a2b2 100644 --- a/config.go +++ b/config.go @@ -16,6 +16,7 @@ package sctfe import ( "crypto" + "crypto/ecdsa" "errors" "fmt" "strings" @@ -32,7 +33,6 @@ type ValidatedLogConfig struct { // is also its submission prefix, as per https://c2sp.org/static-ct-api. Origin string // Used to sign the checkpoint and SCTs. - // TODO(phboneff): check that this is RSA or ECDSA only. Signer crypto.Signer // If set, ExtKeyUsages will restrict the set of such usages that the // server will accept. By default all are accepted. The values specified @@ -90,6 +90,16 @@ func ValidateLogConfig(origin string, projectID string, bucket string, spannerDB return nil, errors.New("empty rootsPemFile") } + // Validate signer that only ECDSA is supported. + if signer == nil { + return nil, errors.New("empty signer") + } + switch keyType := signer.Public().(type) { + case *ecdsa.PublicKey: + default: + return nil, fmt.Errorf("unsupported key type: %v", keyType) + } + lExtKeyUsages := []string{} lRejectExtensions := []string{} if extKeyUsages != "" { diff --git a/deployment/live/gcp/test/.terraform.lock.hcl b/deployment/live/gcp/test/.terraform.lock.hcl new file mode 100644 index 00000000..c0334c4e --- /dev/null +++ b/deployment/live/gcp/test/.terraform.lock.hcl @@ -0,0 +1,41 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.1.0" + constraints = "6.1.0" + hashes = [ + "h1:okppWOAoIPz45VkydzAA74HRLgEKvP4CFXypPU228j8=", + "zh:2463510438c97c59e06ab1fb1ef76221c844abd1bc404c439401fc256e9928ab", + "zh:2afd9b76a81c51632bd54d3cc3bdc2685e8d89b8ace8ca7578b1ae42880228b5", + "zh:51e2fb64c7c8258ac0ec7315d488e5c655b392bf565f9bee2922ee72f6abfb90", + "zh:85aa39bad51132810ee6cd369f426614abff59cb0274fc737d087c17afa9b5ee", + "zh:989669bfed5ca7bf4d960eb9f27a62cbe2578ca2907da7c74fc93edae9a497fa", + "zh:a26665782e90ef3fd322d6a23a1de383c81ae93395e7c2bd9648a1aa85c69876", + "zh:d5e1b785b4c8569b91153eeba89280ffbbe7a0aaabb708833ada67544aeed057", + "zh:d748c69eab6acc4ab7ec369b3bd3ddd5d2e4120d99570743dafde74934959a20", + "zh:eb853ab5c4c0d3e536b8c77abf844b7893ac355967c95b6e0d39b12526e67989", + "zh:f4b50f0ae082412ba189041b6ac540523b7d6463905fed63be67eec03e1539b9", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f6e7adcfafe267d9c657a6c087388f7e0c1e3be4dc179a9a823f75c830a499b7", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.6" + hashes = [ + "h1:dYSb3V94K5dDMtrBRLPzBpkMTPn+3cXZ/kIJdtFL+2M=", + "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", + "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", + "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", + "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", + "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", + "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", + "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", + "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", + "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", + "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", + "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} \ No newline at end of file diff --git a/deployment/live/gcp/test/README.md b/deployment/live/gcp/test/README.md index 8ba6c03f..8fa83388 100644 --- a/deployment/live/gcp/test/README.md +++ b/deployment/live/gcp/test/README.md @@ -38,6 +38,13 @@ Terraforming the project can be done by: 1. `cd` to the relevant directory for the environment to deploy/change (e.g. `ci`) 2. Run `terragrunt apply` +Store the Secret Manager resource ID of signer key pair into the environment variables: + +```sh +export SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID=$(terragrunt output -raw ecdsa_p256_public_key_id) +export SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID=$(terragrunt output -raw ecdsa_p256_private_key_id) +``` + ## Run the SCTFE ### With fake chains @@ -50,10 +57,10 @@ go run ./cmd/gcp/ \ --bucket=${GOOGLE_PROJECT}-${TESSERA_BASE_NAME}-bucket \ --spanner_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-db \ --spanner_dedup_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-dedup-db \ - --private_key=./testdata/ct-http-server.privkey.pem \ - --password=dirk \ --roots_pem_file=./testdata/fake-ca.cert \ - --origin=${TESSERA_BASE_NAME} + --origin=${TESSERA_BASE_NAME} \ + --signer_public_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID} \ + --signer_private_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID} ``` In a different terminal you can either mint and submit certificates manually, or @@ -131,7 +138,6 @@ go run ./client/ctclient get-roots --log_uri=${SRC_LOG_URI} --text=false > /tmp/ sed -i 's-""-"/tmp/hammercfg/roots.pem"-g' /tmp/hammercfg/hammer.cfg ``` - Run the SCTFE with the same roots: ```bash @@ -140,11 +146,11 @@ go run ./cmd/gcp/ \ --project_id=${GOOGLE_PROJECT} \ --bucket=${GOOGLE_PROJECT}-${TESSERA_BASE_NAME}-bucket \ --spanner_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-db \ - --private_key=./testdata/ct-http-server.privkey.pem \ - --password=dirk \ --roots_pem_file=/tmp/hammercfg/roots.pem \ --origin=${TESSERA_BASE_NAME} \ --spanner_dedup_db_path=projects/${GOOGLE_PROJECT}/instances/${TESSERA_BASE_NAME}/databases/${TESSERA_BASE_NAME}-dedup-db \ + --signer_public_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PUBLIC_KEY_ID} \ + --signer_private_key_secret_name=${SCTFE_SIGNER_ECDSA_P256_PRIVATE_KEY_ID} \ -v=3 ``` diff --git a/deployment/modules/gcp/storage/main.tf b/deployment/modules/gcp/storage/main.tf index 61d2f1ce..c6332e33 100644 --- a/deployment/modules/gcp/storage/main.tf +++ b/deployment/modules/gcp/storage/main.tf @@ -65,3 +65,55 @@ resource "google_spanner_database" "dedup_db" { "CREATE TABLE IDSeq (id INT64 NOT NULL, h BYTES(MAX) NOT NULL, idx INT64 NOT NULL,) PRIMARY KEY (id, h)", ] } + +# Secret Manager + +# ECDSA key with P256 elliptic curve. Do NOT use this in production environment. +# +# Security Notice +# The private key generated by this resource will be stored unencrypted in your +# Terraform state file. Use of this resource for production deployments is not +# recommended. Instead, generate a private key file outside of Terraform and +# distribute it securely to the system where Terraform will be run. +# +# See https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key. +resource "tls_private_key" "sctfe_ecdsa_p256" { + algorithm = "ECDSA" + ecdsa_curve = "P256" +} + +resource "google_secret_manager_secret" "sctfe_ecdsa_p256_public_key" { + secret_id = "sctfe-ecdsa-p256-public-key" + + labels = { + label = "sctfe-public-key" + } + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "sctfe_ecdsa_p256_public_key" { + secret = google_secret_manager_secret.sctfe_ecdsa_p256_public_key.id + + secret_data = tls_private_key.sctfe_ecdsa_p256.public_key_pem +} + +resource "google_secret_manager_secret" "sctfe_ecdsa_p256_private_key" { + secret_id = "sctfe-ecdsa-p256-private-key" + + labels = { + label = "sctfe-private-key" + } + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "sctfe_ecdsa_p256_private_key" { + secret = google_secret_manager_secret.sctfe_ecdsa_p256_private_key.id + + secret_data = tls_private_key.sctfe_ecdsa_p256.private_key_pem +} diff --git a/deployment/modules/gcp/storage/outputs.tf b/deployment/modules/gcp/storage/outputs.tf index c2239766..77658f4c 100644 --- a/deployment/modules/gcp/storage/outputs.tf +++ b/deployment/modules/gcp/storage/outputs.tf @@ -17,3 +17,13 @@ output "dedup_spanner_db" { description = "Dedup Spanner database" value = google_spanner_database.dedup_db } + +output "ecdsa_p256_public_key_id" { + description = "Signer public key (P256_SHA256)" + value = google_secret_manager_secret_version.sctfe_ecdsa_p256_public_key.id +} + +output "ecdsa_p256_private_key_id" { + description = "Signer private key (P256_SHA256)" + value = google_secret_manager_secret_version.sctfe_ecdsa_p256_private_key.id +} diff --git a/go.mod b/go.mod index 63725578..6124d1cf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/transparency-dev/static-ct go 1.23.1 require ( + cloud.google.com/go/secretmanager v1.14.2 cloud.google.com/go/spanner v1.71.0 cloud.google.com/go/storage v1.46.0 github.com/golang/mock v1.6.0 diff --git a/go.sum b/go.sum index 3ac808c5..19ce166e 100644 --- a/go.sum +++ b/go.sum @@ -490,6 +490,8 @@ cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISI cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/secretmanager v1.14.2 h1:2XscWCfy//l/qF96YE18/oUaNJynAx749Jg3u0CjQr8= +cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=