Commit 9640989e authored by Geoff Simmons's avatar Geoff Simmons

Support JSON marshaling of information about PEM files.

parent 7b8647a5
......@@ -2,6 +2,7 @@ module code.uplex.de/k8s/k8s-crt-dnldr
require (
github.com/sirupsen/logrus v1.6.0
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f
k8s.io/api v0.16.4
k8s.io/apimachinery v0.16.4
......
/*
* Copyright (c) 2020 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package pem
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"golang.org/x/crypto/ed25519"
)
// Secret represents the Kubernetes metadata of a TLS Secret, suitable
// for JSON marshaling.
type Secret struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
UID string `json:"uid"`
Version string `json:"resourceVersion"`
}
// FileMeta represents file system metadata of a PEM file, suitable
// for JSON marshaling.
type FileMeta struct {
ModTime time.Time `json:"mtime"`
Size int64 `json:"size"`
}
// Validity represents validity constraints from an X509 certificate,
// suitable for JSON marshaling. This means in particular that the
// algorithm is identified by a "string enum", and binary data are
// represented as strings of colon-separated hex bytes.
type Validity struct {
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
}
// PubKeyInfo encapsulates information about a public key in an X500
// certificate, suitable for JSON marshaling.
type PubKeyInfo struct {
Algorithm string `json:"algorithm"`
KeyInfo interface{} `json:"keyInfo"`
}
// RSAKeyInfo represents information about an RSA public key, suitable
// for JSON marshaling.
type RSAKeyInfo struct {
Bits int `json:"size"`
Modulus string `json:"modulus"`
Exponent int `json:"exponent"`
}
// DSAKeyInfo represents information about a DSA public key, suitable
// for JSON marshaling.
type DSAKeyInfo struct {
Pub string `json:"pub"`
P string `json:"P"`
Q string `json:"Q"`
G string `json:"G"`
}
// ECDSAKeyInfo represents information about an ECDSA public key,
// suitable for JSON marshaling.
type ECDSAKeyInfo struct {
Name string `json:"name"`
X string `json:"X"`
Y string `json:"Y"`
}
// ED25519KeyInfo represents an ed25519 public key as a string of
// colon-separated hex bytes.
type ED25519KeyInfo string
// UnknownKeyInfo is a placeholder type for public keys that could not
// be parsed from a X509 certificate.
type UnknownKeyInfo struct{}
// UnmarshalJSON extracts an instance of RSAKeyInfo, DSAKeyInfo,
// ECDSAKeyInfo, ED25519KeyInfo or UnknownKeyInfo from a JSON
// string. The type is determined by the value of the field
// "algorithm", and depends on the capabilities of the go crypto and
// X509 libraries.
func (info *PubKeyInfo) UnmarshalJSON(b []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
algoVal, ok := m["algorithm"]
if !ok {
return errors.New("algorithm not found")
}
keyInfoVal, ok := m["keyInfo"]
if !ok {
return errors.New("keyInfo not found")
}
keyInfo, ok := keyInfoVal.(map[string]interface{})
if !ok {
return errors.New("keyInfo not a map[string]string")
}
algo, ok := algoVal.(string)
if !ok {
return errors.New("algorithm: not a string")
}
info.Algorithm = algo
switch algo {
case x509.UnknownPublicKeyAlgorithm.String():
info.KeyInfo = UnknownKeyInfo{}
case x509.RSA.String():
bitsVal, ok := keyInfo["size"]
if !ok {
return errors.New("rsa keyInfo: bits not found")
}
bits, ok := bitsVal.(float64)
if !ok {
return fmt.Errorf(
"rsa keyInfo bits: not a float64: %T",
bitsVal)
}
modulusVal, ok := keyInfo["modulus"]
if !ok {
return errors.New("rsa keyInfo: modulus not found")
}
modulus, ok := modulusVal.(string)
if !ok {
return fmt.Errorf(
"rsa keyInfo modulus: not a string: %T",
modulus)
}
exponentVal, ok := keyInfo["exponent"]
if !ok {
return errors.New("rsa keyInfo: exponent not found")
}
exponent, ok := exponentVal.(float64)
if !ok {
return fmt.Errorf(
"rsa keyInfo exponent: not a float64: %T",
exponentVal)
}
info.KeyInfo = RSAKeyInfo{
Bits: int(bits),
Modulus: modulus,
Exponent: int(exponent),
}
case x509.DSA.String():
pubVal, ok := keyInfo["pub"]
if !ok {
return errors.New("dsa keyInfo: pub not found")
}
pub, ok := pubVal.(string)
if !ok {
return fmt.Errorf("dsa keyInfo pub: not a string: %T",
pub)
}
pVal, ok := keyInfo["P"]
if !ok {
return errors.New("dsa keyInfo: P not found")
}
p, ok := pVal.(string)
if !ok {
return fmt.Errorf("dsa keyInfo P: not a string: %T", p)
}
qVal, ok := keyInfo["Q"]
if !ok {
return errors.New("dsa keyInfo: Q not found")
}
q, ok := qVal.(string)
if !ok {
return fmt.Errorf("dsa keyInfo Q: not a string: %T", q)
}
gVal, ok := keyInfo["G"]
if !ok {
return errors.New("dsa keyInfo: G not found")
}
g, ok := gVal.(string)
if !ok {
return fmt.Errorf("dsa keyInfo G: not a string: %T", g)
}
info.KeyInfo = DSAKeyInfo{
Pub: pub,
P: p,
Q: q,
G: g,
}
case x509.ECDSA.String():
nameVal, ok := keyInfo["name"]
if !ok {
return errors.New("ecdsa keyInfo: name not found")
}
name, ok := nameVal.(string)
if !ok {
return fmt.Errorf(
"ecdsa keyInfo name: not a string: %T", name)
}
xVal, ok := keyInfo["X"]
if !ok {
return errors.New("ecdsa keyInfo: x not found")
}
x, ok := xVal.(string)
if !ok {
return fmt.Errorf(
"ecdsa keyInfo x: not a string: %T", x)
}
yVal, ok := keyInfo["Y"]
if !ok {
return errors.New("ecdsa keyInfo: y not found")
}
y, ok := yVal.(string)
if !ok {
return fmt.Errorf(
"ecdsa keyInfo y: not a string: %T", y)
}
info.KeyInfo = ECDSAKeyInfo{
Name: name,
X: x,
Y: y,
}
default:
info.KeyInfo = UnknownKeyInfo{}
}
return nil
}
func bytes2HexColonString(bytes []byte) string {
s := make([]string, len(bytes))
for i, b := range bytes {
s[i] = fmt.Sprintf("%02x", b)
}
return strings.Join(s, ":")
}
func bigInt2HexColonString(bigInt *big.Int) string {
return bytes2HexColonString(bigInt.Bytes())
}
func getPubKeyInfo(algo x509.PublicKeyAlgorithm, key interface{}) PubKeyInfo {
info := PubKeyInfo{Algorithm: algo.String()}
if algo == x509.UnknownPublicKeyAlgorithm {
info.Algorithm = "unknown"
}
switch k := key.(type) {
case *rsa.PublicKey:
info.KeyInfo = RSAKeyInfo{
Bits: k.Size() * 8,
Modulus: bigInt2HexColonString(k.N),
Exponent: k.E,
}
case *dsa.PublicKey:
info.KeyInfo = DSAKeyInfo{
Pub: bigInt2HexColonString(k.Y),
P: bigInt2HexColonString(k.P),
Q: bigInt2HexColonString(k.Q),
G: bigInt2HexColonString(k.G),
}
case *ecdsa.PublicKey:
info.KeyInfo = ECDSAKeyInfo{
Name: k.Params().Name,
X: bigInt2HexColonString(k.X),
Y: bigInt2HexColonString(k.Y),
}
case ed25519.PublicKey:
info.KeyInfo = ED25519KeyInfo(bytes2HexColonString(k))
default:
info.KeyInfo = UnknownKeyInfo{}
}
return info
}
var keyUsages = []string{
"digitalSignature",
"contentCommitment",
"keyEncipherment",
"dataEncipherment",
"keyAgreement",
"keyCertSign",
"cRLSign",
"encipherOnly",
"decipherOnly",
}
var extKeyUsages = map[x509.ExtKeyUsage]string{
x509.ExtKeyUsageAny: "any",
x509.ExtKeyUsageServerAuth: "serverAuth",
x509.ExtKeyUsageClientAuth: "clientAuth",
x509.ExtKeyUsageCodeSigning: "codeSigning",
x509.ExtKeyUsageEmailProtection: "emailProtection",
x509.ExtKeyUsageIPSECEndSystem: "IPSECEndSystem",
x509.ExtKeyUsageIPSECTunnel: "IPSECTunnel",
x509.ExtKeyUsageIPSECUser: "IPSECUser",
x509.ExtKeyUsageTimeStamping: "timeStamping",
x509.ExtKeyUsageOCSPSigning: "OCSPSigning",
x509.ExtKeyUsageMicrosoftServerGatedCrypto: "microsoftServerGatedCrypto",
x509.ExtKeyUsageNetscapeServerGatedCrypto: "netscapeServerGatedCrypto",
x509.ExtKeyUsageMicrosoftCommercialCodeSigning: "microsoftCommercialCodeSigning",
x509.ExtKeyUsageMicrosoftKernelCodeSigning: "microsoftKernelCodeSigning",
}
var sigAlgo = map[x509.SignatureAlgorithm]string{
x509.UnknownSignatureAlgorithm: "unknown",
x509.MD2WithRSA: "md2WithRSAEncryption",
x509.MD5WithRSA: "md5WithRSAEncryption",
x509.SHA1WithRSA: "sha1WithRSAEncryption",
x509.SHA256WithRSA: "sha256WithRSAEncryption",
x509.SHA384WithRSA: "sha384WithRSAEncryption",
x509.SHA512WithRSA: "sha512WithRSAEncryption",
x509.DSAWithSHA1: "dsaWithSHA1",
x509.DSAWithSHA256: "dsaWithSHA256",
x509.ECDSAWithSHA1: "ecdsaWithSHA1",
x509.ECDSAWithSHA256: "ecdsaWithSHA256",
x509.ECDSAWithSHA384: "ecdsaWithSHA384",
x509.ECDSAWithSHA512: "ecdsaWithSHA512",
x509.SHA256WithRSAPSS: "sha256WithRSAPSS",
x509.SHA384WithRSAPSS: "sha384WithRSAPSS",
x509.SHA512WithRSAPSS: "sha512WithRSAPSS",
}
func oid2String(oid asn1.ObjectIdentifier) string {
decimals := make([]string, len(oid))
for i, num := range oid {
decimals[i] = strconv.Itoa(num)
}
return strings.Join(decimals, ".")
}
// BasicConstraints may represent basic constraints from an X509
// certificate, if Valid is true. This object is always present in
// struct Crt, but does not in fact represent the certificate's
// contents if Valid is false. Suitable for JSON marshaling.
type BasicConstraints struct {
Valid bool `json:"valid"`
IsCA bool `json:"cA"`
MaxPath int `json:"pathLenConstraint"`
}
// SigInfo represents information about the signature in an X509
// certificate, suitable for JSON marshaling. The algorithm is
// identified by a "string enum", and the signature data is encoded as
// a colon-separated string of hex bytes.
type SigInfo struct {
Algorithm string `json:"algorithm"`
Signature string `json:"signature"`
}
// Crt represents contents of an X509 certificate, to the extent that
// the certificate could be parsed by facilities of go's crypto/x509
// package, suitable for JSON marshaling.
type Crt struct {
Version int `json:"version"`
SN string `json:"serialNumber"`
Issuer string `json:"issuer"`
Valid Validity `json:"validity"`
Subject string `json:"subject"`
PubKey PubKeyInfo `json:"publicKeyInfo"`
SubjectKeyID string `json:"subjectKeyId,omitempty"`
AuthorityKeyID string `json:"authorityKeyId,omitempty"`
Basic BasicConstraints `json:"basicConstraints"`
KeyUsage []string `json:"keyUsage,omitempty"`
ExtKeyUsage []string `json:"extendedKeyUsage,omitempty"`
UnknownExtKeyUsage []string `json:"unknownExtKeyUsage,omitempty"`
OCSPServer []string `json:"ocsp,omitempty"`
IssuingCertificateURL []string `json:"caIssuers,omitempty"`
DNSNames []string `json:"subjectAltDNSNames,omitempty"`
EmailAddresses []string `json:"subjectAltEmails,omitempty"`
IPAddresses []string `json:"subjectAltIPs,omitempty"`
URIs []string `json:"subjectAltURIs,omitempty"`
NameConstraintsCritical bool `json:"nameConstraintsCritical,omitempty"`
PermittedDNS []string `json:"permittedDNS,omitempty"`
ExcludedDNS []string `json:"excludedDNS,omitempty"`
PermittedIPRanges []string `json:"permittedIPRanges,omitempty"`
ExcludedIPRanges []string `json:"excludedIPRanges,omitempty"`
PermittedEmails []string `json:"permittedEmails,omitempty"`
ExcludedEmails []string `json:"excludedEmails,omitempty"`
PermittedURIs []string `json:"PermittedURIs,omitempty"`
ExcludedURIs []string `json:"PermittedURIs,omitempty"`
CRLDistributionPoints []string `json:"cRLDistributionPoints,omitempty"`
PolicyIdentifiers []string `json:"policyConstraints,omitempty"`
SigInfo SigInfo `json:"signatureInfo"`
PEM string `json:"pem"`
}
// FileInfo is the top-level type containing information about a PEM
// file, suitable for JSON marshaling.
type FileInfo struct {
K8sSecret Secret `json:"secret"`
Meta FileMeta `json:"fileMeta"`
Cert Crt `json:"certificate"`
}
func mkCrt(data []byte) (Crt, error) {
crt := Crt{}
block, _ := pem.Decode(data)
if block == nil {
return crt, errors.New("Cannot decode PEM data")
}
if block.Type != "CERTIFICATE" && block.Type != "X509 CERTIFICATE" &&
block.Type != "X.509 CERTIFICATE" {
return crt, errors.New("Not a certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return crt, err
}
crt.Version = cert.Version
crt.SN = bigInt2HexColonString(cert.SerialNumber)
crt.Issuer = cert.Issuer.String()
crt.Subject = cert.Subject.String()
crt.Valid.NotBefore = cert.NotBefore
crt.Valid.NotAfter = cert.NotAfter
crt.PubKey = getPubKeyInfo(cert.PublicKeyAlgorithm, cert.PublicKey)
crt.SubjectKeyID = bytes2HexColonString(cert.SubjectKeyId)
crt.AuthorityKeyID = bytes2HexColonString(cert.AuthorityKeyId)
if cert.BasicConstraintsValid {
crt.Basic.Valid = true
crt.Basic.IsCA = cert.IsCA
crt.Basic.MaxPath = cert.MaxPathLen
if cert.MaxPathLen == 0 && !cert.MaxPathLenZero {
crt.Basic.MaxPath = -1
}
} else {
crt.Basic.MaxPath = -1
}
if cert.KeyUsage != 0 {
crt.KeyUsage = make([]string, 0, len(keyUsages))
for bit, u := range keyUsages {
if uint(cert.KeyUsage)&(1<<uint(bit)) != 0 {
crt.KeyUsage = append(crt.KeyUsage, u)
}
}
}
if len(cert.ExtKeyUsage) > 0 {
crt.ExtKeyUsage = make([]string, len(cert.ExtKeyUsage))
for i, k := range cert.ExtKeyUsage {
crt.ExtKeyUsage[i] = extKeyUsages[k]
}
}
if len(cert.UnknownExtKeyUsage) > 0 {
crt.UnknownExtKeyUsage =
make([]string, len(cert.UnknownExtKeyUsage))
for i := range cert.UnknownExtKeyUsage {
crt.UnknownExtKeyUsage[i] = oid2String(
cert.UnknownExtKeyUsage[i])
}
}
crt.OCSPServer = cert.OCSPServer
crt.IssuingCertificateURL = cert.IssuingCertificateURL
crt.DNSNames = cert.DNSNames
crt.EmailAddresses = cert.EmailAddresses
if len(cert.IPAddresses) > 0 {
crt.IPAddresses = make([]string, len(cert.IPAddresses))
for i := range cert.IPAddresses {
crt.IPAddresses[i] = cert.IPAddresses[i].String()
}
}
if len(cert.URIs) > 0 {
crt.URIs = make([]string, len(cert.URIs))
for i := range cert.URIs {
crt.URIs[i] = cert.URIs[i].String()
}
}
crt.PermittedDNS = cert.PermittedDNSDomains
crt.ExcludedDNS = cert.ExcludedDNSDomains
if len(cert.PermittedIPRanges) > 0 {
crt.PermittedIPRanges =
make([]string, len(cert.PermittedIPRanges))
for i := range cert.PermittedIPRanges {
crt.PermittedIPRanges[i] =
cert.PermittedIPRanges[i].String()
}
}
if len(cert.ExcludedIPRanges) > 0 {
crt.ExcludedIPRanges =
make([]string, len(cert.ExcludedIPRanges))
for i := range cert.ExcludedIPRanges {
crt.ExcludedIPRanges[i] =
cert.ExcludedIPRanges[i].String()
}
}
crt.PermittedEmails = cert.PermittedEmailAddresses
crt.ExcludedEmails = cert.ExcludedEmailAddresses
crt.PermittedURIs = cert.PermittedURIDomains
crt.ExcludedURIs = cert.ExcludedURIDomains
crt.CRLDistributionPoints = cert.CRLDistributionPoints
if len(cert.PolicyIdentifiers) > 0 {
crt.PolicyIdentifiers =
make([]string, len(cert.PolicyIdentifiers))
for i := range cert.PolicyIdentifiers {
crt.PolicyIdentifiers[i] = oid2String(
cert.PolicyIdentifiers[i])
}
}
var ok bool
if crt.SigInfo.Algorithm, ok = sigAlgo[cert.SignatureAlgorithm]; !ok {
crt.SigInfo.Algorithm = "unknown"
}
crt.SigInfo.Signature = bytes2HexColonString(cert.Signature)
end, delim := []byte("-----END "), []byte("-----")
endIdx := bytes.Index(data, end)
delimIdx := bytes.Index(data[endIdx+len(end):], delim)
lastIdx := endIdx + len(end) + delimIdx + len(delim)
crtOnly := data[:lastIdx]
crtOnly = append(crtOnly, byte('\n'))
crt.PEM = string(crtOnly)
return crt, nil
}
/*
* Copyright (c) 2020 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package pem
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sync"
"testing"
"time"
)
var basedir string
func TestMain(m *testing.M) {
_, filename, _, ok := runtime.Caller(0)
if !ok {
fmt.Fprintln(os.Stderr, "Cannot get test directory")
os.Exit(-1)
}
curDir := filepath.Dir(filename)
basedir = filepath.Join(curDir, "testdata")
os.Exit(m.Run())
}
const (
whiskeyPem = `-----BEGIN CERTIFICATE-----
MIIFtTCCA52gAwIBAgIUL23Y5IPw50RNOsyXUg4Yk9elHw0wDQYJKoZIhvcNAQEL
BQAwajELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hh
bWJ1cmcxHTAbBgNVBAoMFFRoZSBOZXh0IFdoaXNrZXkgQmFyMRgwFgYDVQQDDA9i
YXIuZXhhbXBsZS5jb20wHhcNMjAwNTEzMTI0NDMzWhcNNDAwNTA4MTI0NDMzWjBq
MQswCQYDVQQGEwJERTEQMA4GA1UECAwHSGFtYnVyZzEQMA4GA1UEBwwHSGFtYnVy
ZzEdMBsGA1UECgwUVGhlIE5leHQgV2hpc2tleSBCYXIxGDAWBgNVBAMMD2Jhci5l
eGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK4LQefd
TO7Q14fhxfWwZ3pM/VUCS1SEo2+efFx11kwXUY4h3R3uV8Ezx7advFnMWANsxA1s
NUNnVoPZ6jZ17DIp/oBbBHUbZ0m/UlH4frQluZX4RfbOtdHGjns1HtfZOkE7qW1J
qht2taLdCjxGob8vJc/EBfKtp4Tbl9U1rlk9E2rxk1KVpVoxgKrgnmbABYDb/yA+
KsDqHmPLnQiVeZ3CoflI9aFMAmcNnw18CfGfnUWyd5vVSgUCSyaw28bTwRJtWhWD
6fB9Rus2E5k5VaBqAL49bFharlV4zivfc2vcXdah1/W5+Vrp/vW/OgpU1p4s7454
kyItYZb2GGKxKVtH1tNatb3X+aY7aFfYTAiSeeCtpkMV1a14sA6B3jw3e7+UzwSc
E9/ljgV+h/JOtbxOexYRCZJel3OLxfKKq9YrdH2tUjCTWJhZH91wzRWLcidNlFL0
I1Q8xlu1Cif46piYTBkgyco4Mj3UHxSpQnVG4+3A7ZT5alemS4iukOZIUEs97CuI
RCC9UgKDHDzXE3GN0Qh7syWiyvqszdrCnB9iLNsv52eBC4DG1D65C9wY/1VdI+k1
6vJ+LqWh2aoKMcctLiq6o6z32uJff3FUkB8q8bWQ/OemDwtfWt9WW3qxRy9NvBlX
N7w1YejTlrAeIAEehZszgbBaDQXUaa60cngHAgMBAAGjUzBRMB0GA1UdDgQWBBTu
4JHYJOrVGPlooPrLZL2TzDeUhjAfBgNVHSMEGDAWgBTu4JHYJOrVGPlooPrLZL2T
zDeUhjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBvaUAyKqMv
1/77rX8bOkBSgscl2T50/Em4HOXo0WFTV6Lef3LIbNNu5xASf/TVX9Ckfrr42CKP
vS1B/HF27L+kB1cmd76BaaLd2mqJ9SYiww8y7N280IzoVUTc72kEzCVUtY/y9zCJ
olbt8zetZ0B4w5cba0TqRQYScDCAWmqnRUGF37IDSlvN3bNnoGOv8PVFsvswtn1l
pIKuyCyO1wCk7BPkdltLXysxe2m+cfIbosdCBKpCKj+iso1FqrPVXaoHiVHGvc9C
36vge9gNhR69sbrePbQrEB1mKp3HVf38qp0mlinOcNbwdRVxwaK33Q7kDO2w7JL9
oucFgd8w/HNqNU/HiemKPKjXrrJGQGQDltvtGEhnWLro8ez0bZZqANSnWLZdXpJX
84Lhb58bMuxBG9jnUc2wcmMbjiISpq8oGhajUAATnkc/B8B1vHZ73lNSdIUL61VA
o7lOZrYW6PSGh1QixHa7D1Nid5hcj6aaymNKyi7ESj5XTlbqJaBb+8zeVOR64HxJ
BFJG0FzRjk/TheVL8aO1Y7cj8woPcWGJj0ZJhBY6kuEN44nv7NbsXkiW4hJ1wHVQ
gWLNsQYCwyES3pIgliBkog54uFMGjpyeUJeATBcZpkvztjXS9GrVQhI6L3jEgN0C
4sa0SgvX/NR/L52KBSUWb4PX+VYWHw01nA==
-----END CERTIFICATE-----
`
cafePem = `-----BEGIN CERTIFICATE-----
MIIDWTCCAkECFHb8EN0l0QwiR4eKKIW6h172z+JrMA0GCSqGSIb3DQEBCwUAMGgx
CzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdIYW1idXJnMRAwDgYDVQQHDAdIYW1idXJn
MRowGAYDVQQKDBFHcmVlbiBNaWRnZXQgQ2FmZTEZMBcGA1UEAwwQY2FmZS5leGFt
cGxlLmNvbTAgFw0yMDA1MDQxNzA5NTlaGA8yMTIwMDQxMDE3MDk1OVowaDELMAkG
A1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hhbWJ1cmcxGjAY
BgNVBAoMEUdyZWVuIE1pZGdldCBDYWZlMRkwFwYDVQQDDBBjYWZlLmV4YW1wbGUu
Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAts0HCq6fq9gv0uEa
3iOruZ3GnctdCoeGjrQQ4Fh2cQoMm/i3pkDUt6x2pLTQhxlN3oH3WEo1a24r/3S8
Xfy6Xf0Pti+dDiCqAwMd6veu56RItVMO1pmx1wDjGFTuplpnPRtz8EKsaKYfjZd1
BabdhkWhsA9g3nns8+lqeNbvebhk7hiv9lpgDWAnBie+hioan4WQdPZm1/bANH6o
+oWDu1o6Gdrk/iaj2pR73VTFsR2UEmSTpXa35W7/nsmgADIc4RovU+9ho1I4/fSy
jgVlZVBz29yLaDyNuoZljzNhvGqq1wW6Jq/v1uBOPxNH1k3ZQJl4jlG0tsoASnm7
mr9hewIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBMNCVYMTdlaNaTjJ5Cznk9Gd+u
TSIFmOCetTOt3l0Xe0bSTxboT6Oz9nFDMP2A2HRK/GTp25ec+Ek1iiCIF47RcsGp
Cdug+x4wQVP3pxakJ/odFN1ReZGZCjNwBltxlRXwJhArK5PWmQppmMZPrW1UYW8y
x+m5UREzOzWga6EIlhpMEfgNa0BNCL/2gPaz2MpKXq5We93IDe2O0nlRrrVoDHU2
GFMhTpWSLkloaMzIMlcKR0IGyezG9waVgsliS00bYKp8eRJ5SqCUYvCMuApjoyzW
N2w59p6t5xE7Ktb0cmhZg83ISPTBlGqVJxF0clLob5nWyeutXNkP/KOi38PI
-----END CERTIFICATE-----
`
ecdsaPem = `-----BEGIN CERTIFICATE-----
MIIBHjCBxaADAgECAgEBMAoGCCqGSM49BAMCMBcxFTATBgNVBAoTDERvY2tlciwg
SW5jLjAeFw0xMzA3MjUwMTEwMjRaFw0xNTA3MjUwMTEwMjRaMBcxFTATBgNVBAoT
DERvY2tlciwgSW5jLjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMolCWAO0iP7
tkX/KLjQ9CKeOoHYynBgfFcd1ZGoxcefmIbWjHx29eWI3xlhbjS6ssSxhrw1Kuh5
RrASfUCHD7SjAjAAMAoGCCqGSM49BAMCA0gAMEUCIQDRLQTSSeqjsxsb+q4exLSt
EM7f7/ymBzoUzbXU7wI9AgIgXCWaI++GkopGT8T2qV/3+NL0U+fYM0ZjSNSiwaK3
+kA=
-----END CERTIFICATE-----
`
ed25519Pem = `-----BEGIN CERTIFICATE-----
MIIBLDCB36ADAgECAghWAUdKKo3DMDAFBgMrZXAwGTEXMBUGA1UEAwwOSUVURiBUZX
N0IERlbW8wHhcNMTYwODAxMTIxOTI0WhcNNDAxMjMxMjM1OTU5WjAZMRcwFQYDVQQD
DA5JRVRGIFRlc3QgRGVtbzAqMAUGAytlbgMhAIUg8AmJMKdUdIt93LQ+91oNvzoNJj
ga9OukqY6qm05qo0UwQzAPBgNVHRMBAf8EBTADAQEAMA4GA1UdDwEBAAQEAwIDCDAg
BgNVHQ4BAQAEFgQUmx9e7e0EM4Xk97xiPFl1uQvIuzswBQYDK2VwA0EAryMB/t3J5v
/BzKc9dNZIpDmAgs3babFOTQbs+BolzlDUwsPrdGxO3YNGhW7Ibz3OGhhlxXrCe1Cg
w1AH9efZBw==
-----END CERTIFICATE-----
`
)
var (
expWhiskeyCrt = Crt{
Version: 3,
SN: "2f:6d:d8:e4:83:f0:e7:44:4d:3a:cc:97:52:0e:18:93:d7:a5:1f:0d",
Issuer: "CN=bar.example.com,O=The Next Whiskey Bar,L=Hamburg,ST=Hamburg,C=DE",
Valid: Validity{
NotBefore: time.Date(2020, 5, 13, 12, 44, 33, 0, time.UTC),
NotAfter: time.Date(2040, 5, 8, 12, 44, 33, 0, time.UTC),
},
Subject: "CN=bar.example.com,O=The Next Whiskey Bar,L=Hamburg,ST=Hamburg,C=DE",
PubKey: PubKeyInfo{
Algorithm: "RSA",
KeyInfo: RSAKeyInfo{
Bits: 4096,
Modulus: "ae:0b:41:e7:dd:4c:ee:d0:d7:87:e1:c5:f5:b0:67:7a:4c:fd:55:02:4b:54:84:a3:6f:9e:7c:5c:75:d6:4c:17:51:8e:21:dd:1d:ee:57:c1:33:c7:b6:9d:bc:59:cc:58:03:6c:c4:0d:6c:35:43:67:56:83:d9:ea:36:75:ec:32:29:fe:80:5b:04:75:1b:67:49:bf:52:51:f8:7e:b4:25:b9:95:f8:45:f6:ce:b5:d1:c6:8e:7b:35:1e:d7:d9:3a:41:3b:a9:6d:49:aa:1b:76:b5:a2:dd:0a:3c:46:a1:bf:2f:25:cf:c4:05:f2:ad:a7:84:db:97:d5:35:ae:59:3d:13:6a:f1:93:52:95:a5:5a:31:80:aa:e0:9e:66:c0:05:80:db:ff:20:3e:2a:c0:ea:1e:63:cb:9d:08:95:79:9d:c2:a1:f9:48:f5:a1:4c:02:67:0d:9f:0d:7c:09:f1:9f:9d:45:b2:77:9b:d5:4a:05:02:4b:26:b0:db:c6:d3:c1:12:6d:5a:15:83:e9:f0:7d:46:eb:36:13:99:39:55:a0:6a:00:be:3d:6c:58:5a:ae:55:78:ce:2b:df:73:6b:dc:5d:d6:a1:d7:f5:b9:f9:5a:e9:fe:f5:bf:3a:0a:54:d6:9e:2c:ef:8e:78:93:22:2d:61:96:f6:18:62:b1:29:5b:47:d6:d3:5a:b5:bd:d7:f9:a6:3b:68:57:d8:4c:08:92:79:e0:ad:a6:43:15:d5:ad:78:b0:0e:81:de:3c:37:7b:bf:94:cf:04:9c:13:df:e5:8e:05:7e:87:f2:4e:b5:bc:4e:7b:16:11:09:92:5e:97:73:8b:c5:f2:8a:ab:d6:2b:74:7d:ad:52:30:93:58:98:59:1f:dd:70:cd:15:8b:72:27:4d:94:52:f4:23:54:3c:c6:5b:b5:0a:27:f8:ea:98:98:4c:19:20:c9:ca:38:32:3d:d4:1f:14:a9:42:75:46:e3:ed:c0:ed:94:f9:6a:57:a6:4b:88:ae:90:e6:48:50:4b:3d:ec:2b:88:44:20:bd:52:02:83:1c:3c:d7:13:71:8d:d1:08:7b:b3:25:a2:ca:fa:ac:cd:da:c2:9c:1f:62:2c:db:2f:e7:67:81:0b:80:c6:d4:3e:b9:0b:dc:18:ff:55:5d:23:e9:35:ea:f2:7e:2e:a5:a1:d9:aa:0a:31:c7:2d:2e:2a:ba:a3:ac:f7:da:e2:5f:7f:71:54:90:1f:2a:f1:b5:90:fc:e7:a6:0f:0b:5f:5a:df:56:5b:7a:b1:47:2f:4d:bc:19:57:37:bc:35:61:e8:d3:96:b0:1e:20:01:1e:85:9b:33:81:b0:5a:0d:05:d4:69:ae:b4:72:78:07",
Exponent: 65537,
},
},
SubjectKeyID: "ee:e0:91:d8:24:ea:d5:18:f9:68:a0:fa:cb:64:bd:93:cc:37:94:86",
AuthorityKeyID: "ee:e0:91:d8:24:ea:d5:18:f9:68:a0:fa:cb:64:bd:93:cc:37:94:86",
Basic: BasicConstraints{
Valid: true,
IsCA: true,
MaxPath: -1,
},
SigInfo: SigInfo{
Algorithm: "sha256WithRSAEncryption",
Signature: "6f:69:40:32:2a:a3:2f:d7:fe:fb:ad:7f:1b:3a:40:52:82:c7:25:d9:3e:74:fc:49:b8:1c:e5:e8:d1:61:53:57:a2:de:7f:72:c8:6c:d3:6e:e7:10:12:7f:f4:d5:5f:d0:a4:7e:ba:f8:d8:22:8f:bd:2d:41:fc:71:76:ec:bf:a4:07:57:26:77:be:81:69:a2:dd:da:6a:89:f5:26:22:c3:0f:32:ec:dd:bc:d0:8c:e8:55:44:dc:ef:69:04:cc:25:54:b5:8f:f2:f7:30:89:a2:56:ed:f3:37:ad:67:40:78:c3:97:1b:6b:44:ea:45:06:12:70:30:80:5a:6a:a7:45:41:85:df:b2:03:4a:5b:cd:dd:b3:67:a0:63:af:f0:f5:45:b2:fb:30:b6:7d:65:a4:82:ae:c8:2c:8e:d7:00:a4:ec:13:e4:76:5b:4b:5f:2b:31:7b:69:be:71:f2:1b:a2:c7:42:04:aa:42:2a:3f:a2:b2:8d:45:aa:b3:d5:5d:aa:07:89:51:c6:bd:cf:42:df:ab:e0:7b:d8:0d:85:1e:bd:b1:ba:de:3d:b4:2b:10:1d:66:2a:9d:c7:55:fd:fc:aa:9d:26:96:29:ce:70:d6:f0:75:15:71:c1:a2:b7:dd:0e:e4:0c:ed:b0:ec:92:fd:a2:e7:05:81:df:30:fc:73:6a:35:4f:c7:89:e9:8a:3c:a8:d7:ae:b2:46:40:64:03:96:db:ed:18:48:67:58:ba:e8:f1:ec:f4:6d:96:6a:00:d4:a7:58:b6:5d:5e:92:57:f3:82:e1:6f:9f:1b:32:ec:41:1b:d8:e7:51:cd:b0:72:63:1b:8e:22:12:a6:af:28:1a:16:a3:50:00:13:9e:47:3f:07:c0:75:bc:76:7b:de:53:52:74:85:0b:eb:55:40:a3:b9:4e:66:b6:16:e8:f4:86:87:54:22:c4:76:bb:0f:53:62:77:98:5c:8f:a6:9a:ca:63:4a:ca:2e:c4:4a:3e:57:4e:56:ea:25:a0:5b:fb:cc:de:54:e4:7a:e0:7c:49:04:52:46:d0:5c:d1:8e:4f:d3:85:e5:4b:f1:a3:b5:63:b7:23:f3:0a:0f:71:61:89:8f:46:49:84:16:3a:92:e1:0d:e3:89:ef:ec:d6:ec:5e:48:96:e2:12:75:c0:75:50:81:62:cd:b1:06:02:c3:21:12:de:92:20:96:20:64:a2:0e:78:b8:53:06:8e:9c:9e:50:97:80:4c:17:19:a6:4b:f3:b6:35:d2:f4:6a:d5:42:12:3a:2f:78:c4:80:dd:02:e2:c6:b4:4a:0b:d7:fc:d4:7f:2f:9d:8a:05:25:16:6f:83:d7:f9:56:16:1f:0d:35:9c",
},
PEM: whiskeyPem,
}
expCafeCrt = Crt{
Version: 1,
SN: "76:fc:10:dd:25:d1:0c:22:47:87:8a:28:85:ba:87:5e:f6:cf:e2:6b",
Issuer: "CN=cafe.example.com,O=Green Midget Cafe,L=Hamburg,ST=Hamburg,C=DE",
Valid: Validity{
NotBefore: time.Date(2020, 5, 4, 17, 9, 59, 0, time.UTC),
NotAfter: time.Date(2120, 4, 10, 17, 9, 59, 0, time.UTC),
},
Subject: "CN=cafe.example.com,O=Green Midget Cafe,L=Hamburg,ST=Hamburg,C=DE",
PubKey: PubKeyInfo{
Algorithm: "RSA",
KeyInfo: RSAKeyInfo{
Bits: 2048,
Modulus: "b6:cd:07:0a:ae:9f:ab:d8:2f:d2:e1:1a:de:23:ab:b9:9d:c6:9d:cb:5d:0a:87:86:8e:b4:10:e0:58:76:71:0a:0c:9b:f8:b7:a6:40:d4:b7:ac:76:a4:b4:d0:87:19:4d:de:81:f7:58:4a:35:6b:6e:2b:ff:74:bc:5d:fc:ba:5d:fd:0f:b6:2f:9d:0e:20:aa:03:03:1d:ea:f7:ae:e7:a4:48:b5:53:0e:d6:99:b1:d7:00:e3:18:54:ee:a6:5a:67:3d:1b:73:f0:42:ac:68:a6:1f:8d:97:75:05:a6:dd:86:45:a1:b0:0f:60:de:79:ec:f3:e9:6a:78:d6:ef:79:b8:64:ee:18:af:f6:5a:60:0d:60:27:06:27:be:86:2a:1a:9f:85:90:74:f6:66:d7:f6:c0:34:7e:a8:fa:85:83:bb:5a:3a:19:da:e4:fe:26:a3:da:94:7b:dd:54:c5:b1:1d:94:12:64:93:a5:76:b7:e5:6e:ff:9e:c9:a0:00:32:1c:e1:1a:2f:53:ef:61:a3:52:38:fd:f4:b2:8e:05:65:65:50:73:db:dc:8b:68:3c:8d:ba:86:65:8f:33:61:bc:6a:aa:d7:05:ba:26:af:ef:d6:e0:4e:3f:13:47:d6:4d:d9:40:99:78:8e:51:b4:b6:ca:00:4a:79:bb:9a:bf:61:7b",
Exponent: 65537,
},
},
Basic: BasicConstraints{
Valid: false,
IsCA: false,
MaxPath: -1,
},
SigInfo: SigInfo{
Algorithm: "sha256WithRSAEncryption",
Signature: "4c:34:25:58:31:37:65:68:d6:93:8c:9e:42:ce:79:3d:19:df:ae:4d:22:05:98:e0:9e:b5:33:ad:de:5d:17:7b:46:d2:4f:16:e8:4f:a3:b3:f6:71:43:30:fd:80:d8:74:4a:fc:64:e9:db:97:9c:f8:49:35:8a:20:88:17:8e:d1:72:c1:a9:09:db:a0:fb:1e:30:41:53:f7:a7:16:a4:27:fa:1d:14:dd:51:79:91:99:0a:33:70:06:5b:71:95:15:f0:26:10:2b:2b:93:d6:99:0a:69:98:c6:4f:ad:6d:54:61:6f:32:c7:e9:b9:51:11:33:3b:35:a0:6b:a1:08:96:1a:4c:11:f8:0d:6b:40:4d:08:bf:f6:80:f6:b3:d8:ca:4a:5e:ae:56:7b:dd:c8:0d:ed:8e:d2:79:51:ae:b5:68:0c:75:36:18:53:21:4e:95:92:2e:49:68:68:cc:c8:32:57:0a:47:42:06:c9:ec:c6:f7:06:95:82:c9:62:4b:4d:1b:60:aa:7c:79:12:79:4a:a0:94:62:f0:8c:b8:0a:63:a3:2c:d6:37:6c:39:f6:9e:ad:e7:11:3b:2a:d6:f4:72:68:59:83:cd:c8:48:f4:c1:94:6a:95:27:11:74:72:52:e8:6f:99:d6:c9:eb:ad:5c:d9:0f:fc:a3:a2:df:c3:c8",
},
PEM: cafePem,
}
expEcdsaCrt = Crt{
Version: 3,
SN: "01",
Issuer: "O=Docker\\, Inc.",
Valid: Validity{
NotBefore: time.Date(2013, 7, 25, 1, 10, 24, 0, time.UTC),
NotAfter: time.Date(2015, 7, 25, 1, 10, 24, 0, time.UTC),
},
Subject: "O=Docker\\, Inc.",
PubKey: PubKeyInfo{
Algorithm: "ECDSA",
KeyInfo: ECDSAKeyInfo{
Name: "P-256",
X: "ca:25:09:60:0e:d2:23:fb:b6:45:ff:28:b8:d0:f4:22:9e:3a:81:d8:ca:70:60:7c:57:1d:d5:91:a8:c5:c7:9f",
Y: "98:86:d6:8c:7c:76:f5:e5:88:df:19:61:6e:34:ba:b2:c4:b1:86:bc:35:2a:e8:79:46:b0:12:7d:40:87:0f:b4",
},
},
Basic: BasicConstraints{
Valid: false,
IsCA: false,
MaxPath: -1,
},
SigInfo: SigInfo{
Algorithm: "ecdsaWithSHA256",
Signature: "30:45:02:21:00:d1:2d:04:d2:49:ea:a3:b3:1b:1b:fa:ae:1e:c4:b4:ad:10:ce:df:ef:fc:a6:07:3a:14:cd:b5:d4:ef:02:3d:02:02:20:5c:25:9a:23:ef:86:92:8a:46:4f:c4:f6:a9:5f:f7:f8:d2:f4:53:e7:d8:33:46:63:48:d4:a2:c1:a2:b7:fa:40",
},
PEM: ecdsaPem,
}
expEd25519Crt = Crt{
Version: 3,
SN: "56:01:47:4a:2a:8d:c3:30",
Issuer: "CN=IETF Test Demo",
Valid: Validity{
NotBefore: time.Date(2016, 8, 1, 12, 19, 24, 0, time.UTC),
NotAfter: time.Date(2040, 12, 31, 23, 59, 59, 0, time.UTC),
},
Subject: "CN=IETF Test Demo",
PubKey: PubKeyInfo{
Algorithm: "unknown",
KeyInfo: UnknownKeyInfo{},
},
SubjectKeyID: "9b:1f:5e:ed:ed:04:33:85:e4:f7:bc:62:3c:59:75:b9:0b:c8:bb:3b",
Basic: BasicConstraints{
Valid: true,
IsCA: false,
MaxPath: -1,
},
KeyUsage: []string{"keyAgreement"},
SigInfo: SigInfo{
Algorithm: "unknown",
Signature: "af:23:01:fe:dd:c9:e6:ff:c1:cc:a7:3d:74:d6:48:a4:39:80:82:cd:db:69:b1:4e:4d:06:ec:f8:1a:25:ce:50:d4:c2:c3:eb:74:6c:4e:dd:83:46:85:6e:c8:6f:3d:ce:1a:18:65:c5:7a:c2:7b:50:a0:c3:50:07:f5:e7:d9:07",
},
PEM: ed25519Pem,
}
)
func TestMkCrt(t *testing.T) {
crt, err := mkCrt([]byte(whiskeyPem))
if err != nil {
t.Fatalf("mkCrt(whiskeyPem): %v", err)
}
if !reflect.DeepEqual(crt, expWhiskeyCrt) {
t.Errorf("mkCrt(whiskyPem) got %#v want %#v", crt,
expWhiskeyCrt)
}
jsonBytes, err := json.Marshal(crt)
if err != nil {
t.Fatalf("json.Marshal(crt): %v", err)
}
if err = json.Unmarshal(jsonBytes, &crt); err != nil {
t.Fatalf("json.Unmarshal(whiskeyJson): %v", err)
}
if !reflect.DeepEqual(crt, expWhiskeyCrt) {
t.Errorf("json.Unmarshal(whiskeyJson) got %#v want %#v", crt,
expWhiskeyCrt)
}
crt, err = mkCrt([]byte(cafePem))
if err != nil {
t.Fatalf("mkCrt(cafePem): %v", err)
}
if !reflect.DeepEqual(crt, expCafeCrt) {
t.Errorf("mkCrt(cafePem) got %#v want %#v", crt, expCafeCrt)
}
jsonBytes, err = json.Marshal(crt)
if err != nil {
t.Fatalf("json.Marshal(crt): %v", err)
}
if err = json.Unmarshal(jsonBytes, &crt); err != nil {
t.Fatalf("json.Unmarshal(cafeJson): %v", err)
}
if !reflect.DeepEqual(crt, expCafeCrt) {
t.Errorf("json.Unmarshal(cafeJson) got %#v want %#v", crt,
expCafeCrt)
}
crt, err = mkCrt([]byte(ecdsaPem))
if err != nil {
t.Fatalf("mkCrt(ecdsaPem): %v", err)
}
if !reflect.DeepEqual(crt, expEcdsaCrt) {
t.Errorf("mkCrt(ecdsaPem) got %#v want %#v", crt, expEcdsaCrt)
}
jsonBytes, err = json.Marshal(crt)
if err != nil {
t.Fatalf("json.Marshal(crt): %v", err)
}
if err = json.Unmarshal(jsonBytes, &crt); err != nil {
t.Fatalf("json.Unmarshal(ecdsaJson): %v", err)
}
if !reflect.DeepEqual(crt, expEcdsaCrt) {
t.Errorf("json.Unmarshal(ecdsaJson) got %#v want %#v", crt,
expEcdsaCrt)
}
crt, err = mkCrt([]byte(ed25519Pem))
if err != nil {
t.Fatalf("mkCrt(ed25519Pem): %v", err)
}
if !reflect.DeepEqual(crt, expEd25519Crt) {
t.Errorf("mkCrt(ed25519Pem) got %#v want %#v", crt,
expEd25519Crt)
}
jsonBytes, err = json.Marshal(crt)
if err != nil {
t.Fatalf("json.Marshal(crt): %v", err)
}
if err = json.Unmarshal(jsonBytes, &crt); err != nil {
t.Fatalf("json.Unmarshal(ed25519Json): %v", err)
}
if !reflect.DeepEqual(crt, expEd25519Crt) {
t.Errorf("json.Unmarshal(ed25519Json) got %#v want %#v", crt,
expEd25519Crt)
}
}
var fileInfo = FileInfo{
K8sSecret: Secret{
Namespace: "ns",
Name: "name",
UID: "1dc443a3-e23d-49e6-b016-fc8b1eaeb724",
Version: "4711",
},
Meta: FileMeta{
ModTime: time.Date(2020, 7, 27, 9, 40, 26, 0, time.UTC),
Size: 1147,
},
Cert: expWhiskeyCrt,
}
func TestFileInfoJSON(t *testing.T) {
jsonBytes, err := json.Marshal(fileInfo)
if err != nil {
t.Fatalf("json.Marshal(fileInfo): %v", err)
}
var unmarshalledFileInfo FileInfo
if err = json.Unmarshal(jsonBytes, &unmarshalledFileInfo); err != nil {
t.Fatalf("json.Unmarshal(fileInfoJSON): %v", err)
}
if !reflect.DeepEqual(unmarshalledFileInfo, fileInfo) {
t.Errorf("json.Unmarshal(fileInfoJSON) got %#v want %#v",
unmarshalledFileInfo, fileInfo)
}
}
var pemFiles = &Files{
Files: map[string]*File{
"ns1/cafe": &File{
Namespace: "ns1",
Name: "cafe",
UID: "975d4e4f-9ea9-49d9-a81d-1b4bca92a743",
ResourceVersion: "4711",
Size: 2899,
ModTime: time.Date(2020, 7, 27, 9, 50, 0, 0, time.UTC),
},
"ns2/bar": &File{
Namespace: "ns2",
Name: "bar",
UID: "d18974c5-94d7-4e04-b2af-6e9274ad46d8",
ResourceVersion: "0815",
Size: 5313,
ModTime: time.Date(2020, 7, 27, 9, 51, 0, 0, time.UTC),
},
},
Gid: -1,
mtx: new(sync.RWMutex),
}
var invalidFiles = &Files{
Files: map[string]*File{
"invalid/enoent": &File{
Namespace: "invalid",
Name: "enoent",
},
"invalid/crt": &File{
Namespace: "invalid",
Name: "crt",
},
},
Gid: -1,
mtx: new(sync.RWMutex),
}
var (
expCafeInfo = FileInfo{
K8sSecret: Secret{
Namespace: "ns1",
Name: "cafe",
UID: "975d4e4f-9ea9-49d9-a81d-1b4bca92a743",
Version: "4711",
},
Meta: FileMeta{
Size: 2899,
ModTime: time.Date(2020, 7, 27, 9, 50, 0, 0, time.UTC),
},
Cert: expCafeCrt,
}
expBarInfo = FileInfo{
K8sSecret: Secret{
Namespace: "ns2",
Name: "bar",
UID: "d18974c5-94d7-4e04-b2af-6e9274ad46d8",
Version: "0815",
},
Meta: FileMeta{
Size: 5313,
ModTime: time.Date(2020, 7, 27, 9, 51, 0, 0, time.UTC),
},
Cert: expWhiskeyCrt,
}
)
func TestGetFileInfo(t *testing.T) {
pemFiles.Base = basedir
info, err := pemFiles.GetFileInfo("ns1", "cafe", "", "")
if err != nil {
t.Fatalf("GetFileInfo(ns1/cafe): %v", err)
}
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GetFileInfo(ns1/cafe) got %#v want %#v",
info, expCafeInfo)
}
jsonBytes, err := json.Marshal(info)
if err != nil {
t.Fatalf("json.Marshal(info): %v", err)
}
var unmarshalledFileInfo FileInfo
if err = json.Unmarshal(jsonBytes, &unmarshalledFileInfo); err != nil {
t.Fatalf("json.Unmarshal(fileInfoJSON): %v", err)
}
if !reflect.DeepEqual(unmarshalledFileInfo, info) {
t.Errorf("json.Unmarshal(fileInfoJSON) got %#v want %#v",
unmarshalledFileInfo, info)
}
info, err = pemFiles.GetFileInfo("ns2", "bar", "", "")
if err != nil {
t.Fatalf("GetFileInfo(ns2/bar): %v", err)
}
if !reflect.DeepEqual(info, expBarInfo) {
t.Errorf("GetFileInfo(ns2/bar) got %#v want %#v",
info, expBarInfo)
}
jsonBytes, err = json.Marshal(info)
if err != nil {
t.Fatalf("json.Marshal(info): %v", err)
}
if err = json.Unmarshal(jsonBytes, &unmarshalledFileInfo); err != nil {
t.Fatalf("json.Unmarshal(fileInfoJSON): %v", err)
}
if !reflect.DeepEqual(unmarshalledFileInfo, info) {
t.Errorf("json.Unmarshal(fileInfoJSON) got %#v want %#v",
unmarshalledFileInfo, info)
}
if info, err = pemFiles.GetFileInfo("ns1", "bar", "", ""); err == nil {
t.Error("GetFileInfo(ns1/bar) error got nil want not found")
}
if info, err = pemFiles.GetFileInfo("ns2", "cafe", "", ""); err == nil {
t.Error("GetFileInfo(ns2/cafe) error got nil want not found")
}
if info, err = pemFiles.GetFileInfo("ns1", "cafe",
"975d4e4f-9ea9-49d9-a81d-1b4bca92a743", ""); err != nil {
t.Errorf("GetFileInfo(ns1/cafe,uid) error got %v want nil", err)
}
if info, err = pemFiles.GetFileInfo(
"ns1", "cafe", "", "4711"); err != nil {
t.Errorf("GetFileInfo(ns1/cafe,version) error got %v want nil",
err)
}
if info, err = pemFiles.GetFileInfo("ns1", "cafe",
"975d4e4f-9ea9-49d9-a81d-1b4bca92a743", "4711"); err != nil {
t.Errorf("GetFileInfo(ns1/cafe,uid,version) error got %v "+
"want nil", err)
}
if info, err = pemFiles.GetFileInfo("ns1", "cafe",
"d18974c5-94d7-4e04-b2af-6e9274ad46d8", ""); err == nil {
t.Error("GetFileInfo(ns1/cafe,uid) error got nil " +
"want not found")
}
if info, err = pemFiles.GetFileInfo(
"ns1", "cafe", "", "0815"); err == nil {
t.Error("GetFileInfo(ns1/cafe,version) error got nil " +
"want nil")
}
if info, err = pemFiles.GetFileInfo("ns1", "cafe",
"d18974c5-94d7-4e04-b2af-6e9274ad46d8", "0815"); err == nil {
t.Error("GetFileInfo(ns1/cafe,uid,version) error got nil " +
"want not found")
}
invalidFiles.Base = basedir
if _, err = invalidFiles.GetFileInfo(
"invalid", "enoent", "", ""); err == nil {
t.Errorf("GetFileInfo(invalid/enoent) error got nil " +
"want not found")
}
if _, err = invalidFiles.GetFileInfo(
"invalid", "crt", "", ""); err == nil {
t.Errorf("GetFileInfo(invalid/crt) error got nil " +
"want crt parse error")
}
}
func TestGetAllFileInfo(t *testing.T) {
pemFiles.Base = basedir
allInfo, err := pemFiles.GetAllFileInfo()
if err != nil {
t.Fatalf("GetAllFileInfo(): %v", err)
}
if len(allInfo) != 2 {
t.Fatalf("GetAllFileInfo() len(slice) got %d want 2",
len(allInfo))
}
cafeFound, barFound := false, false
for _, info := range allInfo {
secret := info.K8sSecret
if secret.Namespace == "ns1" && secret.Name == "cafe" {
cafeFound = true
if !reflect.DeepEqual(info, expCafeInfo) {
t.Errorf("GetAllFileInfo() cafe got %#v "+
"want %#v", info, expCafeInfo)
}
} else if secret.Namespace == "ns2" && secret.Name == "bar" {
barFound = true
if !reflect.DeepEqual(info, expBarInfo) {
t.Errorf("GetAllFileInfo() bar got %#v "+
"want %#v", info, expBarInfo)
}
} else {
t.Errorf("GetAllFileInfo() unexpected: %#v", info)
}
}
if !cafeFound {
t.Errorf("GetAllFileInfo() did not return cafe: %#v", allInfo)
}
if !barFound {
t.Errorf("GetAllFileInfo() did not return bar: %#v", allInfo)
}
jsonBytes, err := json.Marshal(allInfo)
if err != nil {
t.Fatalf("json.Marshal(allInfo): %v", err)
}
var unmarshalledAllInfo []FileInfo
if err = json.Unmarshal(jsonBytes, &unmarshalledAllInfo); err != nil {
t.Fatalf("json.Unmarshal(allInfoJSON): %v", err)
}
if !reflect.DeepEqual(unmarshalledAllInfo, allInfo) {
t.Errorf("json.Unmarshal(allInfoJSON) got %#v want %#v",
unmarshalledAllInfo, allInfo)
}
invalidFiles.Base = basedir
if _, err = invalidFiles.GetAllFileInfo(); err == nil {
t.Errorf("invalid.GetAllFileInfo() error got nil want non-nil")
}
}
......@@ -361,3 +361,84 @@ func (pemfiles *Files) Delete(namespace, name string) (bool, error) {
delete(pemfiles.Files, key)
return true, nil
}
// GetFileInfo returns information suitable for JSON marshaling about
// a PEM file, if a file is currently stored for the given namespace
// and name, and optionally matching the UID and ResourceVersion.
//
// ns and name MUST be set, while uid and/or version MAY be empty. If
// either of uid or version is set, then the FileInfo is returned only
// if the UID and/or ResourceVersion of the corresponding Secret
// match.
//
// If an entry is found, the PEM is read and its contents are parsed
// to populate the Cert field, to extent supported by go's crypto/x509
// facilities.
//
// GetFileInfo is protected by a read lock.
func (pemfiles *Files) GetFileInfo(
ns, name, uid, version string,
) (FileInfo, error) {
info := FileInfo{}
mapKey := ns + "/" + name
pemfiles.mtx.RLock()
defer pemfiles.mtx.RUnlock()
pemFile, exists := pemfiles.Files[mapKey]
if !exists {
return info, fmt.Errorf("%s not found", mapKey)
}
if uid != "" && pemFile.UID != uid {
return info, fmt.Errorf("%s uid=%s not found", mapKey, uid)
}
if version != "" && pemFile.ResourceVersion != version {
return info, fmt.Errorf("%s version=%s not found", mapKey,
version)
}
path := pemfiles.Path(pemFile)
pemBytes, err := ioutil.ReadFile(path)
if err != nil {
return info, fmt.Errorf("%s: cannot read pem file: %v", mapKey,
err)
}
crt, err := mkCrt(pemBytes)
if err != nil {
return info, fmt.Errorf("%s: cannot read certificate: %v",
mapKey, err)
}
info.K8sSecret = Secret{
Namespace: pemFile.Namespace,
Name: pemFile.Name,
UID: pemFile.UID,
Version: pemFile.ResourceVersion,
}
info.Meta = FileMeta{
ModTime: pemFile.ModTime,
Size: pemFile.Size,
}
info.Cert = crt
return info, nil
}
// GetAllFileInfo returns information about all of the PEM files
// currently stored, suitable for JSON marshaling. Is is protected by
// a read lock.
func (pemfiles *Files) GetAllFileInfo() ([]FileInfo, error) {
allInfo := make([]FileInfo, 0, len(pemfiles.Files))
pemfiles.mtx.RLock()
defer pemfiles.mtx.RUnlock()
for _, pemFile := range pemfiles.Files {
info, err := pemfiles.GetFileInfo(
pemFile.Namespace, pemFile.Name, "", "")
if err != nil {
return allInfo, err
}
allInfo = append(allInfo, info)
}
return allInfo, nil
}
-----BEGIN CERTIFICATE-----
DEADBEEF
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
FOOBAR
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDWTCCAkECFHb8EN0l0QwiR4eKKIW6h172z+JrMA0GCSqGSIb3DQEBCwUAMGgx
CzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdIYW1idXJnMRAwDgYDVQQHDAdIYW1idXJn
MRowGAYDVQQKDBFHcmVlbiBNaWRnZXQgQ2FmZTEZMBcGA1UEAwwQY2FmZS5leGFt
cGxlLmNvbTAgFw0yMDA1MDQxNzA5NTlaGA8yMTIwMDQxMDE3MDk1OVowaDELMAkG
A1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hhbWJ1cmcxGjAY
BgNVBAoMEUdyZWVuIE1pZGdldCBDYWZlMRkwFwYDVQQDDBBjYWZlLmV4YW1wbGUu
Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAts0HCq6fq9gv0uEa
3iOruZ3GnctdCoeGjrQQ4Fh2cQoMm/i3pkDUt6x2pLTQhxlN3oH3WEo1a24r/3S8
Xfy6Xf0Pti+dDiCqAwMd6veu56RItVMO1pmx1wDjGFTuplpnPRtz8EKsaKYfjZd1
BabdhkWhsA9g3nns8+lqeNbvebhk7hiv9lpgDWAnBie+hioan4WQdPZm1/bANH6o
+oWDu1o6Gdrk/iaj2pR73VTFsR2UEmSTpXa35W7/nsmgADIc4RovU+9ho1I4/fSy
jgVlZVBz29yLaDyNuoZljzNhvGqq1wW6Jq/v1uBOPxNH1k3ZQJl4jlG0tsoASnm7
mr9hewIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBMNCVYMTdlaNaTjJ5Cznk9Gd+u
TSIFmOCetTOt3l0Xe0bSTxboT6Oz9nFDMP2A2HRK/GTp25ec+Ek1iiCIF47RcsGp
Cdug+x4wQVP3pxakJ/odFN1ReZGZCjNwBltxlRXwJhArK5PWmQppmMZPrW1UYW8y
x+m5UREzOzWga6EIlhpMEfgNa0BNCL/2gPaz2MpKXq5We93IDe2O0nlRrrVoDHU2
GFMhTpWSLkloaMzIMlcKR0IGyezG9waVgsliS00bYKp8eRJ5SqCUYvCMuApjoyzW
N2w59p6t5xE7Ktb0cmhZg83ISPTBlGqVJxF0clLob5nWyeutXNkP/KOi38PI
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAts0HCq6fq9gv0uEa3iOruZ3GnctdCoeGjrQQ4Fh2cQoMm/i3
pkDUt6x2pLTQhxlN3oH3WEo1a24r/3S8Xfy6Xf0Pti+dDiCqAwMd6veu56RItVMO
1pmx1wDjGFTuplpnPRtz8EKsaKYfjZd1BabdhkWhsA9g3nns8+lqeNbvebhk7hiv
9lpgDWAnBie+hioan4WQdPZm1/bANH6o+oWDu1o6Gdrk/iaj2pR73VTFsR2UEmST
pXa35W7/nsmgADIc4RovU+9ho1I4/fSyjgVlZVBz29yLaDyNuoZljzNhvGqq1wW6
Jq/v1uBOPxNH1k3ZQJl4jlG0tsoASnm7mr9hewIDAQABAoIBAES7vsQTeNIijYjb
P0D7ZJx8aKv4RVmqL7wElLvmR1KllqwmztbiVZlibZHssuO5bgAWGizGamOkn0KE
YDduyZyBhKDaMlGXkpVjXKJ20vsiWHxlaJTkYWwYV0tU1A8UuvDNG8DhMPaAUCjr
JAMmBPFxySPsBF5itefYgkJBfvXi7sobaCM6A75D+dBLMeq2q+YbIQH/cAojHYfV
7ypyQ1QaY+wsDiCM6n9Qjk4krmHZ/z39y8mO71ytFcMfJJad8LKM5J4p9Qu99qeb
IRDOT/Sb9QXLXWTeCDv5JWPYyFH2u3e/8GsvQLbXYYbfWLNoU6RDaFSc2wmkOwUH
U8pSCDECgYEA3KIQcme//6B2jP31Coa2f8hsENd0nL+EDR9erXLSUga2l0YNPJZj
W6VnNdaeGq92B7Wxgj+dSeeSBdIRhXwABOHHjruG+gotdRRyoO1ldw7mJjN/q3Wx
A1fpJ+J00S1ZO1FbukKZmR7smTS7i73a8V7At3dyjCG6WxErP3N5NM8CgYEA1Bp5
yYIH8oJmPsuJt501k9nU4SdxxQJpb6uZ9QCBqbEsGkWE3vtLErlU8Rnm2HuirMvD
8Q3OsuoupdCTChrJJ04oL/2r60oTGapeDe4BuRM+DRAZ2trCwXy3nT26bZ/DJtur
Hqvt0tey9ee9MiVHWF2biZejd+KMUxPCCoZVS5UCgYEApbz8m+SCH3Yb+DgB7oFZ
8M3PGCuxxto7SVxKVANQKRwv551Q7jWOt9adnJz3Mdai1JHRoaVF87GISOUQEnUe
0owEy5zlfUlN8oiEv4z1zqUbkJDZFCUZ7wgH9tUvqb7mLCAmxtmm5paLZ19sj0H0
iaMDJA8PtmLTyfswwL5uy5MCgYEArdBMgU+nx5oIw+j0IJ4aK+FUzHYQi4vgb3zG
m7ogh7kDFTxnGHwCF4P9Ed9SB5G5y7ToC4BvJLs4IvX7qUouEaHA2SMeYaDAakXs
8albjBkyvm21Yl3nP7w+lALj5bYIrK1TW701FZVhuJaBurhF8So0rdqwQSxMJkCI
wSs4dskCgYBr1LO3GINSwGHt73ueZDtnvFvO+EFDaOFFbsEd14O1mluM4+WrIZky
inZCvygJWzgHF9LCOpoAZxHykMNrEomidpxViAlpBzb/C5CnpzlfiVBqLN3NvOxG
zdkoq6BiZnznsVgoHyP7TQlUX94ahVT01yZ0njPk2aYVipPWUoHQMQ==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIFtTCCA52gAwIBAgIUL23Y5IPw50RNOsyXUg4Yk9elHw0wDQYJKoZIhvcNAQEL
BQAwajELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hh
bWJ1cmcxHTAbBgNVBAoMFFRoZSBOZXh0IFdoaXNrZXkgQmFyMRgwFgYDVQQDDA9i
YXIuZXhhbXBsZS5jb20wHhcNMjAwNTEzMTI0NDMzWhcNNDAwNTA4MTI0NDMzWjBq
MQswCQYDVQQGEwJERTEQMA4GA1UECAwHSGFtYnVyZzEQMA4GA1UEBwwHSGFtYnVy
ZzEdMBsGA1UECgwUVGhlIE5leHQgV2hpc2tleSBCYXIxGDAWBgNVBAMMD2Jhci5l
eGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK4LQefd
TO7Q14fhxfWwZ3pM/VUCS1SEo2+efFx11kwXUY4h3R3uV8Ezx7advFnMWANsxA1s
NUNnVoPZ6jZ17DIp/oBbBHUbZ0m/UlH4frQluZX4RfbOtdHGjns1HtfZOkE7qW1J
qht2taLdCjxGob8vJc/EBfKtp4Tbl9U1rlk9E2rxk1KVpVoxgKrgnmbABYDb/yA+
KsDqHmPLnQiVeZ3CoflI9aFMAmcNnw18CfGfnUWyd5vVSgUCSyaw28bTwRJtWhWD
6fB9Rus2E5k5VaBqAL49bFharlV4zivfc2vcXdah1/W5+Vrp/vW/OgpU1p4s7454
kyItYZb2GGKxKVtH1tNatb3X+aY7aFfYTAiSeeCtpkMV1a14sA6B3jw3e7+UzwSc
E9/ljgV+h/JOtbxOexYRCZJel3OLxfKKq9YrdH2tUjCTWJhZH91wzRWLcidNlFL0
I1Q8xlu1Cif46piYTBkgyco4Mj3UHxSpQnVG4+3A7ZT5alemS4iukOZIUEs97CuI
RCC9UgKDHDzXE3GN0Qh7syWiyvqszdrCnB9iLNsv52eBC4DG1D65C9wY/1VdI+k1
6vJ+LqWh2aoKMcctLiq6o6z32uJff3FUkB8q8bWQ/OemDwtfWt9WW3qxRy9NvBlX
N7w1YejTlrAeIAEehZszgbBaDQXUaa60cngHAgMBAAGjUzBRMB0GA1UdDgQWBBTu
4JHYJOrVGPlooPrLZL2TzDeUhjAfBgNVHSMEGDAWgBTu4JHYJOrVGPlooPrLZL2T
zDeUhjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBvaUAyKqMv
1/77rX8bOkBSgscl2T50/Em4HOXo0WFTV6Lef3LIbNNu5xASf/TVX9Ckfrr42CKP
vS1B/HF27L+kB1cmd76BaaLd2mqJ9SYiww8y7N280IzoVUTc72kEzCVUtY/y9zCJ
olbt8zetZ0B4w5cba0TqRQYScDCAWmqnRUGF37IDSlvN3bNnoGOv8PVFsvswtn1l
pIKuyCyO1wCk7BPkdltLXysxe2m+cfIbosdCBKpCKj+iso1FqrPVXaoHiVHGvc9C
36vge9gNhR69sbrePbQrEB1mKp3HVf38qp0mlinOcNbwdRVxwaK33Q7kDO2w7JL9
oucFgd8w/HNqNU/HiemKPKjXrrJGQGQDltvtGEhnWLro8ez0bZZqANSnWLZdXpJX
84Lhb58bMuxBG9jnUc2wcmMbjiISpq8oGhajUAATnkc/B8B1vHZ73lNSdIUL61VA
o7lOZrYW6PSGh1QixHa7D1Nid5hcj6aaymNKyi7ESj5XTlbqJaBb+8zeVOR64HxJ
BFJG0FzRjk/TheVL8aO1Y7cj8woPcWGJj0ZJhBY6kuEN44nv7NbsXkiW4hJ1wHVQ
gWLNsQYCwyES3pIgliBkog54uFMGjpyeUJeATBcZpkvztjXS9GrVQhI6L3jEgN0C
4sa0SgvX/NR/L52KBSUWb4PX+VYWHw01nA==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
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
sv2NYF2XBWr0oPa9xPvwOZMWFcKSRzw=
-----END PRIVATE KEY-----
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment