mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
dkim: Implement internal dkim signing and verification
This patch implements internal DKIM signing and verification.
This commit is contained in:
@@ -39,8 +39,15 @@ Usage:
|
||||
chasquid-util [options] print-config
|
||||
Print the current chasquid configuration.
|
||||
|
||||
chasquid-util [options] dkim-keygen <domain> [<selector> <private-key.pem>] [--algo=rsa3072|rsa4096|ed25519]
|
||||
Generate a new DKIM key pair for the domain.
|
||||
chasquid-util [options] dkim-dns <domain> [<selector> <private-key.pem>]
|
||||
Print the DNS TXT record to use for the domain, selector and
|
||||
private key.
|
||||
|
||||
Options:
|
||||
-C=<path>, --configdir=<path> Configuration directory
|
||||
-v Verbose mode
|
||||
`
|
||||
|
||||
// Command-line arguments.
|
||||
@@ -80,6 +87,13 @@ func main() {
|
||||
"aliases-resolve": aliasesResolve,
|
||||
"print-config": printConfig,
|
||||
"domaininfo-remove": domaininfoRemove,
|
||||
"dkim-keygen": dkimKeygen,
|
||||
"dkim-dns": dkimDNS,
|
||||
|
||||
// These exist for testing purposes and may be removed in the future.
|
||||
// Do not rely on them.
|
||||
"dkim-verify": dkimVerify,
|
||||
"dkim-sign": dkimSign,
|
||||
}
|
||||
|
||||
cmd := args["$1"]
|
||||
|
||||
260
cmd/chasquid-util/dkim.go
Normal file
260
cmd/chasquid-util/dkim.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"blitiri.com.ar/go/chasquid/internal/dkim"
|
||||
"blitiri.com.ar/go/chasquid/internal/envelope"
|
||||
"blitiri.com.ar/go/chasquid/internal/normalize"
|
||||
)
|
||||
|
||||
func dkimSign() {
|
||||
domain := args["$2"]
|
||||
selector := args["$3"]
|
||||
keyPath := args["$4"]
|
||||
|
||||
msg, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
Fatalf("%v", err)
|
||||
}
|
||||
msg = normalize.ToCRLF(msg)
|
||||
|
||||
if domain == "" {
|
||||
domain = getDomainFromMsg(msg)
|
||||
}
|
||||
if selector == "" {
|
||||
selector = findSelectorForDomain(domain)
|
||||
}
|
||||
if keyPath == "" {
|
||||
keyPath = keyPathFor(domain, selector)
|
||||
}
|
||||
|
||||
signer := &dkim.Signer{
|
||||
Domain: domain,
|
||||
Selector: selector,
|
||||
Signer: loadPrivateKey(keyPath),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if _, verbose := args["-v"]; verbose {
|
||||
ctx = dkim.WithTraceFunc(ctx,
|
||||
func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
})
|
||||
}
|
||||
|
||||
header, err := signer.Sign(ctx, string(msg))
|
||||
if err != nil {
|
||||
Fatalf("Error signing message: %v", err)
|
||||
}
|
||||
fmt.Printf("DKIM-Signature: %s\r\n",
|
||||
strings.ReplaceAll(header, "\r\n", "\r\n\t"))
|
||||
}
|
||||
|
||||
func dkimVerify() {
|
||||
msg, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
Fatalf("%v", err)
|
||||
}
|
||||
msg = normalize.ToCRLF(msg)
|
||||
|
||||
ctx := context.Background()
|
||||
if _, verbose := args["-v"]; verbose {
|
||||
ctx = dkim.WithTraceFunc(ctx,
|
||||
func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
})
|
||||
}
|
||||
|
||||
results, err := dkim.VerifyMessage(ctx, string(msg))
|
||||
if err != nil {
|
||||
Fatalf("Error verifying message: %v", err)
|
||||
}
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
ar := "Authentication-Results: " + hostname + "\r\n\t"
|
||||
ar += strings.ReplaceAll(
|
||||
results.AuthenticationResults(), "\r\n", "\r\n\t")
|
||||
|
||||
fmt.Println(ar)
|
||||
}
|
||||
|
||||
func dkimDNS() {
|
||||
domain := args["$2"]
|
||||
selector := args["$3"]
|
||||
keyPath := args["$4"]
|
||||
|
||||
if domain == "" {
|
||||
Fatalf("Error: missing domain parameter")
|
||||
}
|
||||
if selector == "" {
|
||||
selector = findSelectorForDomain(domain)
|
||||
}
|
||||
if keyPath == "" {
|
||||
keyPath = keyPathFor(domain, selector)
|
||||
}
|
||||
|
||||
fmt.Println(dnsRecordFor(domain, selector, loadPrivateKey(keyPath)))
|
||||
}
|
||||
|
||||
func dnsRecordFor(domain, selector string, private crypto.Signer) string {
|
||||
public := private.Public()
|
||||
|
||||
var err error
|
||||
algoStr := ""
|
||||
pubBytes := []byte{}
|
||||
switch private.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
algoStr = "rsa"
|
||||
pubBytes, err = x509.MarshalPKIXPublicKey(public)
|
||||
case ed25519.PrivateKey:
|
||||
algoStr = "ed25519"
|
||||
pubBytes = public.(ed25519.PublicKey)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Fatalf("Error marshaling public key: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s._domainkey.%s\tTXT\t\"v=DKIM1; k=%s; p=%s\"",
|
||||
selector, domain,
|
||||
algoStr, base64.StdEncoding.EncodeToString(pubBytes))
|
||||
}
|
||||
|
||||
func dkimKeygen() {
|
||||
domain := args["$2"]
|
||||
selector := args["$3"]
|
||||
keyPath := args["$4"]
|
||||
algo := args["--algo"]
|
||||
|
||||
if domain == "" {
|
||||
Fatalf("Error: missing domain parameter")
|
||||
}
|
||||
if selector == "" {
|
||||
selector = time.Now().UTC().Format("20060102")
|
||||
}
|
||||
if keyPath == "" {
|
||||
keyPath = keyPathFor(domain, selector)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(keyPath); !os.IsNotExist(err) {
|
||||
Fatalf("Error: key already exists at %q", keyPath)
|
||||
}
|
||||
|
||||
var private crypto.Signer
|
||||
var err error
|
||||
switch algo {
|
||||
case "", "rsa3072":
|
||||
private, err = rsa.GenerateKey(rand.Reader, 3072)
|
||||
case "rsa4096":
|
||||
private, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||
case "ed25519":
|
||||
_, private, err = ed25519.GenerateKey(rand.Reader)
|
||||
default:
|
||||
Fatalf("Error: unsupported algorithm %q", algo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Fatalf("Error generating key: %v", err)
|
||||
}
|
||||
|
||||
privB, err := x509.MarshalPKCS8PrivateKey(private)
|
||||
if err != nil {
|
||||
Fatalf("Error marshaling private key: %v", err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
|
||||
if err != nil {
|
||||
Fatalf("Error creating key file %q: %v", keyPath, err)
|
||||
}
|
||||
|
||||
block := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: privB,
|
||||
}
|
||||
if err := pem.Encode(f, block); err != nil {
|
||||
Fatalf("Error PEM-encoding key: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
fmt.Printf("Key written to %q\n\n", keyPath)
|
||||
|
||||
fmt.Println(dnsRecordFor(domain, selector, private))
|
||||
}
|
||||
|
||||
func keyPathFor(domain, selector string) string {
|
||||
return path.Clean(fmt.Sprintf("%s/domains/%s/dkim:%s.pem",
|
||||
configDir, domain, selector))
|
||||
}
|
||||
|
||||
func getDomainFromMsg(msg []byte) string {
|
||||
m, err := mail.ReadMessage(bytes.NewReader(msg))
|
||||
if err != nil {
|
||||
Fatalf("Error parsing message: %v", err)
|
||||
}
|
||||
|
||||
addr, err := mail.ParseAddress(m.Header.Get("From"))
|
||||
if err != nil {
|
||||
Fatalf("Error parsing From: header: %v", err)
|
||||
}
|
||||
|
||||
return envelope.DomainOf(addr.Address)
|
||||
}
|
||||
|
||||
func findSelectorForDomain(domain string) string {
|
||||
glob := path.Clean(configDir + "/domains/" + domain + "/dkim:*.pem")
|
||||
ms, err := filepath.Glob(glob)
|
||||
if err != nil {
|
||||
Fatalf("Error finding DKIM keys: %v", err)
|
||||
}
|
||||
for _, m := range ms {
|
||||
base := filepath.Base(m)
|
||||
selector := strings.TrimPrefix(base, "dkim:")
|
||||
selector = strings.TrimSuffix(selector, ".pem")
|
||||
return selector
|
||||
}
|
||||
|
||||
Fatalf("No DKIM keys found in %q", glob)
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadPrivateKey(path string) crypto.Signer {
|
||||
key, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
Fatalf("Error reading private key from %q: %v", path, err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(key)
|
||||
if block == nil {
|
||||
Fatalf("Error decoding PEM block")
|
||||
}
|
||||
|
||||
switch strings.ToUpper(block.Type) {
|
||||
case "PRIVATE KEY":
|
||||
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
Fatalf("Error parsing private key: %v", err)
|
||||
}
|
||||
return k.(crypto.Signer)
|
||||
default:
|
||||
Fatalf("Unsupported key type: %s", block.Type)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ function check_userdb() {
|
||||
}
|
||||
|
||||
|
||||
rm -rf .config/
|
||||
mkdir -p .config/domains/domain/ .data/domaininfo
|
||||
rm -f .config/chasquid.conf
|
||||
echo 'data_dir: ".data"' >> .config/chasquid.conf
|
||||
|
||||
if ! r print-config > /dev/null; then
|
||||
@@ -57,6 +57,9 @@ if ! ( echo "$C" | grep -E -q "hostname:.*\"$HOSTNAME\"" ); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf .keys/
|
||||
mkdir .keys/
|
||||
|
||||
# Run all the chamuyero tests.
|
||||
for i in *.cmy; do
|
||||
if ! chamuyero "$i" > "$i.log" 2>&1 ; then
|
||||
|
||||
190
cmd/chasquid-util/test_dkim.cmy
Normal file
190
cmd/chasquid-util/test_dkim.cmy
Normal file
@@ -0,0 +1,190 @@
|
||||
# Test dkim-dns subcommand with keys pre-generated by openssl, to validate
|
||||
# interoperability.
|
||||
c = ./chasquid-util dkim-dns example.com sel123 test_openssl_genpkey_ed25519.pem
|
||||
c <- sel123._domainkey.example.com TXT "v=DKIM1; k=ed25519; p=QXNdsDCVOrViGMRh4BIE/IgUCcBEwio3kpJ3e0GAipw="
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util dkim-dns example.com sel123 test_openssl_genpkey_rsa.pem
|
||||
c <- sel123._domainkey.example.com TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAieZWhl7dnxHGyucZS2+dyExPQytj/aY46RXJ4yT3zWY8gh5YkVZ2L1x++7XMzzSg/5FR5bkKYV9Xa+jO6YlhriYKo3ttWSmxU0hDKbG7dpD9Tr7tjCcmKqE1IXetl6DXlQl7LRdmkeIND4gtf9A1zOPLR3/+kvsu1u2cUsEFVs36FqbTe4BYLn2RQlT4IQocT5eVEvoHc5apKuTOKBYThhWRaSZG9YXvsdd1UjngR2Xmizu5e/hj2f3W+9rmRRy1ukmUryuMUHMae2V27Wy1vrHiYoMUA1kQJY+HTG5kMkuatxNui9yjmdqrQUvCIU2Fa5jxJYQTLIz4U0/z4tStRwIDAQAB"
|
||||
c wait 0
|
||||
|
||||
# Generate our own keys, and then check we can parse them with dkim-dns.
|
||||
# Do this once per algorithm (including the default).
|
||||
|
||||
# Default algorithm.
|
||||
c = ./chasquid-util dkim-keygen example.com selDef .keys/test_def.pem
|
||||
c <- Key written to ".keys/test_def.pem"
|
||||
c <-
|
||||
c <~ selDef._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{560,570}=*"
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util dkim-dns example.com selDef .keys/test_def.pem
|
||||
c <~ selDef._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{560,570}=*"
|
||||
c wait 0
|
||||
|
||||
# RSA 3072.
|
||||
c = ./chasquid-util dkim-keygen example.com selRSA3 .keys/test_rsa3.pem --algo=rsa3072
|
||||
c <- Key written to ".keys/test_rsa3.pem"
|
||||
c <-
|
||||
c <~ selRSA3._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{560,570}=*"
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util dkim-dns example.com selRSA3 .keys/test_rsa3.pem
|
||||
c <~ selRSA3._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{560,570}=*"
|
||||
c wait 0
|
||||
|
||||
# RSA 4096.
|
||||
c = ./chasquid-util dkim-keygen example.com selRSA4 .keys/test_rsa4.pem --algo=rsa4096
|
||||
c <- Key written to ".keys/test_rsa4.pem"
|
||||
c <-
|
||||
c <~ selRSA4._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{730,740}=*"
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util dkim-dns example.com selRSA4 .keys/test_rsa4.pem
|
||||
c <~ selRSA4._domainkey.example.com\tTXT\t"v=DKIM1; k=rsa; p=[A-Za-z0-9+/]{730,740}=*"
|
||||
c wait 0
|
||||
|
||||
# Ed25519.
|
||||
c = ./chasquid-util dkim-keygen example.com selED25519 .keys/test_ed25519.pem --algo=ed25519
|
||||
c <- Key written to ".keys/test_ed25519.pem"
|
||||
c <-
|
||||
c <~ selED25519._domainkey.example.com\tTXT\t"v=DKIM1; k=ed25519; p=[A-Za-z0-9+/]{40,50}=*"
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util dkim-dns example.com selED25519 .keys/test_ed25519.pem
|
||||
c <~ selED25519._domainkey.example.com\tTXT\t"v=DKIM1; k=ed25519; p=[A-Za-z0-9+/]{40,50}=*"
|
||||
c wait 0
|
||||
|
||||
# Refuse to overwrite a key file.
|
||||
c = ./chasquid-util dkim-keygen example.com selED25519 .keys/test_ed25519.pem --algo=ed25519
|
||||
c <- Error: key already exists at ".keys/test_ed25519.pem"
|
||||
c wait 1
|
||||
|
||||
# Automatically decide on the selector and key path.
|
||||
c = ./chasquid-util -C=.config dkim-keygen domain --algo=ed25519
|
||||
c <~ Key written to ".config/domains/domain/dkim:[0-9]{8}.pem"
|
||||
c <-
|
||||
c <~ [0-9]{8}._domainkey.domain\tTXT\t"v=DKIM1; k=ed25519; p=[A-Za-z0-9+/]{40,50}=*"
|
||||
c wait 0
|
||||
|
||||
# Custom selector, but automatic key path
|
||||
c = ./chasquid-util -C=.config dkim-keygen domain sel1 --algo=ed25519
|
||||
c <~ Key written to ".config/domains/domain/dkim:sel1.pem"
|
||||
c <-
|
||||
c <~ sel1._domainkey.domain\tTXT\t"v=DKIM1; k=ed25519; p=[A-Za-z0-9+/]{40,50}=*"
|
||||
c wait 0
|
||||
|
||||
# Missing parameters.
|
||||
c = ./chasquid-util -C=.config dkim-keygen
|
||||
c <- Error: missing domain parameter
|
||||
c wait 1
|
||||
|
||||
# Unsupported algorithm
|
||||
c = ./chasquid-util -C=.config dkim-keygen domain s k.pem --algo=xxx666
|
||||
c <- Error: unsupported algorithm "xxx666"
|
||||
c wait 1
|
||||
|
||||
# Automatically find selector and key path.
|
||||
c = ./chasquid-util -C=.config dkim-dns domain
|
||||
c <~ [0-9]{8}._domainkey.domain\tTXT\t"v=DKIM1; k=ed25519; p=[A-Za-z0-9+/]{40,50}=*"
|
||||
c wait 0
|
||||
|
||||
# Require at least a domain.
|
||||
c = ./chasquid-util -C=.config dkim-dns
|
||||
c <- Error: missing domain parameter
|
||||
c wait 1
|
||||
|
||||
# Error reading key.
|
||||
c = ./chasquid-util -C=.config dkim-dns domain unknownsel badkey.pem
|
||||
c <- Error reading private key from "badkey.pem": open badkey.pem: no such file or directory
|
||||
c wait 1
|
||||
|
||||
# No DKIM keys found.
|
||||
c = ./chasquid-util -C=.config dkim-dns unkdomain
|
||||
c <- No DKIM keys found in ".config/domains/unkdomain/dkim:*.pem"
|
||||
c wait 1
|
||||
|
||||
# DKIM signing, with various forms.
|
||||
c = ./chasquid-util -C=.config dkim-sign domain
|
||||
c -> From: user-a@srv-a
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||
c <~ \td=domain; s=\d+; t=\d+;
|
||||
c <~ \th=from:from:subject:date:to:cc:message-id;
|
||||
c <~ \tbh=.*;
|
||||
c <~ \tb=.*
|
||||
c <~ \t .*;
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util -C=.config dkim-sign domain sel1
|
||||
c -> From: user-a@srv-a
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util -C=.config dkim-sign domain selED25519 .keys/test_ed25519.pem
|
||||
c -> From: user-a@srv-a
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util -C=.config dkim-sign
|
||||
c -> From: user-a@domain
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||
c wait 0
|
||||
|
||||
# Bad message for dkim-sign.
|
||||
c = ./chasquid-util -C=.config dkim-sign
|
||||
c -> Invalid message.
|
||||
c close
|
||||
c <- Error parsing message: malformed header line: Invalid message.
|
||||
c wait 1
|
||||
|
||||
c = ./chasquid-util -C=.config dkim-sign
|
||||
c -> From: <not a good address>
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- Error parsing From: header: mail: missing @ in addr-spec
|
||||
c wait 1
|
||||
|
||||
# DKIM verification.
|
||||
# Just check that the attempt was made.
|
||||
c = ./chasquid-util -C=.config dkim-verify
|
||||
c -> From: user-a@srv-a
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <~ Authentication-Results: .*
|
||||
c <~ \t;dkim=none
|
||||
c wait 0
|
||||
|
||||
# Tracing. Just check that there's some output, we don't need byte-for-byte
|
||||
# verification as the contents are not expected to be stable.
|
||||
c = ./chasquid-util -C=.config dkim-sign -v
|
||||
c -> From: user-a@domain
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <~ Signing for domain / \d+ with ed25519-sha256
|
||||
c wait 0
|
||||
|
||||
c = ./chasquid-util -C=.config dkim-verify -v
|
||||
c -> From: user-a@srv-a
|
||||
c ->
|
||||
c -> A little tiny message.
|
||||
c close
|
||||
c <- Found 0 signatures, 0 valid
|
||||
c <~ Authentication-Results: .*
|
||||
c <~ \t;dkim=none
|
||||
c wait 0
|
||||
|
||||
3
cmd/chasquid-util/test_openssl_genpkey_ed25519.pem
Normal file
3
cmd/chasquid-util/test_openssl_genpkey_ed25519.pem
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIBul+k51unaApEcZBmt1i65n09asM/howsN4B1AjNY5V
|
||||
-----END PRIVATE KEY-----
|
||||
28
cmd/chasquid-util/test_openssl_genpkey_rsa.pem
Normal file
28
cmd/chasquid-util/test_openssl_genpkey_rsa.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCJ5laGXt2fEcbK
|
||||
5xlLb53ITE9DK2P9pjjpFcnjJPfNZjyCHliRVnYvXH77tczPNKD/kVHluQphX1dr
|
||||
6M7piWGuJgqje21ZKbFTSEMpsbt2kP1Ovu2MJyYqoTUhd62XoNeVCXstF2aR4g0P
|
||||
iC1/0DXM48tHf/6S+y7W7ZxSwQVWzfoWptN7gFgufZFCVPghChxPl5US+gdzlqkq
|
||||
5M4oFhOGFZFpJkb1he+x13VSOeBHZeaLO7l7+GPZ/db72uZFHLW6SZSvK4xQcxp7
|
||||
ZXbtbLW+seJigxQDWRAlj4dMbmQyS5q3E26L3KOZ2qtBS8IhTYVrmPElhBMsjPhT
|
||||
T/Pi1K1HAgMBAAECggEAJRKywk8wv7oUuqnkh/5K6fVx/bnlmOSeOjOsYg+nOyY4
|
||||
MDceUnxvK45vaRZYKICao/qajOrxWno6U310Wx6fDyWVCJx/KlBmJuCvhb8NifOy
|
||||
1f/IdzxzK1TJpuS426HXM28oGVhIMAIYxssyiEEepaW8Gc3UUAmNbyTUOP9BgzNZ
|
||||
8qH5PA5MTTSiC1ql96b5otKPTlizxT13d3MYeSBN4b31Kb/AYRNSZlyOSBFCwcqf
|
||||
qeZEV4cwILX+58PYwfGGRYQWbCT62ZOs5AWiPt/cH9bZg7Gk1GqNx8HKFYaq+QHq
|
||||
hzXkiAjDZrANuK+xeQERuAWViagtX/qtNsQJwAJP6QKBgQDAJxGCYXxv//eM09uU
|
||||
DBz3jrAvROPylrX+eifoleWtdHnBHXcn9G3uNwOSpVS36PcspeH44w2B/WpzDsWn
|
||||
HjVWP2UmeWvPMZsY81Kxd4KINB/l+z03ctYuus80UJmYH70bkJ2uxLWioU1e/Edf
|
||||
ruMGx16ZdBVOCWJ7BtrUc41dswKBgQC3uGZ9QdVoEMDB7dFKl5foYqHE51p4ruMv
|
||||
Rpb5peFQJIdbbCUSaNN9swtDemktf0OnPyGMNLogGBZ/fhf8N2QX5+OwvQeh01Mu
|
||||
vPCFUZ4sNXv7lPPCwj23SmoMd1Z/RdksAlF8kHVBOsHrNurPUqkbhKLChuiAAKDC
|
||||
S0qdoAKwHQKBgQCsqe6X5BW3ZqEBkNX8wK2+3h7/Or5CHJ9JHmeCHkAWj1Vg7KNH
|
||||
6eJmblTtj1cDM3n4Ss81oIFgz2C6JwoA06pF6A1ydyUjN4YQ84TZJ3TKA1yuggZO
|
||||
Lwi7UO4kKlD6W3rIrDik9OnqS1uFANj55+LlEn21EpSaXOB7gHte8L6U9QKBgEy8
|
||||
I2qbzbPak3gsiacbLCKu15xzeTFA8rjzRend4/7iUvrXb6CB0hwFZWX4wedz6WD4
|
||||
mF2ERF1VUkhL9V6uEAuAGnTeb0qjBnJWDivRDDyw1ikdbLbjBH4DAcpVKfacyPl9
|
||||
umVJvP/St94zoN2ZS/KncofHa2LTYFHmurKde6HtAoGBAIGZHOxJF856GJlq3otA
|
||||
9wGGkNpmlVhHdYYvRKCMRr1FcduCrWFrr5zZT/fb6eHSoCtYjsiqRB/j6STgnBiX
|
||||
2jSsPRadUrpyZOkINTl16vC6Bnv4plfP3VIBQAIoD9ViP0v9w8VrQyIGXWAeSHcu
|
||||
eXZyxHh81OEU8M2hWKZf54UI
|
||||
-----END PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user