From e2e4f2037c6e68130ccb2dfd8e94c16b81e38b15 Mon Sep 17 00:00:00 2001 From: "Greg T. Wallace" Date: Thu, 25 Jan 2024 20:16:37 -0500 Subject: [PATCH] app: restructure and start building p15 output --- .gitignore | 2 +- cek.go | 94 ------------------ cmd/main.go | 7 ++ go.mod | 20 +++- go.sum | 22 ++++- kek.go | 24 ----- main.go | 37 ------- pkg/app/app.go | 65 ++++++++++++ pkg/app/app_config.go | 45 +++++++++ pkg/app/app_logger.go | 47 +++++++++ file_header.go => pkg/app/file_header.go | 4 +- pkg/pkcs15/keyid.go | 35 +++++++ pkg/pkcs15/marshal.go | 66 +++++++++++++ pkg/pkcs15/oids.go | 1 + pkg/pkcs15/pem_decode.go | 120 +++++++++++++++++++++++ pkg/pkcs15/pem_to_p15.go | 36 +++++++ pkg/tools/asn1obj/bitstring.go | 27 +++++ pkg/tools/asn1obj/integer.go | 17 ++++ pkg/tools/asn1obj/misc.go | 27 +++++ pkg/tools/asn1obj/octetstring.go | 23 +++++ pkg/tools/asn1obj/oid.go | 19 ++++ pkg/tools/asn1obj/sequence.go | 26 +++++ pkg/tools/asn1obj/utf8string.go | 16 +++ bitwise.go => pkg/tools/bitwise.go | 10 +- 24 files changed, 622 insertions(+), 168 deletions(-) delete mode 100644 cek.go create mode 100644 cmd/main.go delete mode 100644 kek.go delete mode 100644 main.go create mode 100644 pkg/app/app.go create mode 100644 pkg/app/app_config.go create mode 100644 pkg/app/app_logger.go rename file_header.go => pkg/app/file_header.go (94%) create mode 100644 pkg/pkcs15/keyid.go create mode 100644 pkg/pkcs15/marshal.go create mode 100644 pkg/pkcs15/oids.go create mode 100644 pkg/pkcs15/pem_decode.go create mode 100644 pkg/pkcs15/pem_to_p15.go create mode 100644 pkg/tools/asn1obj/bitstring.go create mode 100644 pkg/tools/asn1obj/integer.go create mode 100644 pkg/tools/asn1obj/misc.go create mode 100644 pkg/tools/asn1obj/octetstring.go create mode 100644 pkg/tools/asn1obj/oid.go create mode 100644 pkg/tools/asn1obj/sequence.go create mode 100644 pkg/tools/asn1obj/utf8string.go rename bitwise.go => pkg/tools/bitwise.go (68%) diff --git a/.gitignore b/.gitignore index 9387b50..0f6d1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ *.pem # ignore test_data folder -/test_data \ No newline at end of file +/_test_data diff --git a/cek.go b/cek.go deleted file mode 100644 index b91e32a..0000000 --- a/cek.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "crypto/cipher" - "crypto/des" - "errors" - "fmt" -) - -// decryptCEK decrypts the encrypted CEK and unwraps the CEK so only the -// original CEK is returned -func decryptCEK(encryptedCEK, encryptedCekSalt, KEK []byte) (CEK []byte, err error) { - // ensure proper var lens, or error - encryptedCEKLen := 24 - CEKSaltLen := 8 - KEKLen := 24 - - if len(encryptedCEK) != encryptedCEKLen { - return nil, errors.New("wrong encrypted CEK length") - } - if len(encryptedCekSalt) != CEKSaltLen { - return nil, errors.New("wrong encrypted CEK's salt length") - } - if len(KEK) != KEKLen { - return nil, errors.New("wrong KEK length") - } - - // 3DES uses block byte size of 8 - blockByteSize := 8 - - // make DES cipher from KEK - kekDesCipher, err := des.NewTripleDESCipher(KEK) - if err != nil { - return nil, fmt.Errorf("failed to make DES cipher for cek decryption (%s)", err) - } - - // (1) first use n-1'th block as IV to decrypt n'th block - ivStart := encryptedCEKLen - 2*blockByteSize - ivEnd := encryptedCEKLen - 1*blockByteSize - - ivBlockCipherText := encryptedCEK[ivStart:ivEnd] - nthBlockCipherText := encryptedCEK[encryptedCEKLen-1*blockByteSize:] - - firstBlockDecrypter := cipher.NewCBCDecrypter(kekDesCipher, ivBlockCipherText) - - decryptedNthBlock := make([]byte, len(nthBlockCipherText)) - firstBlockDecrypter.CryptBlocks(decryptedNthBlock, nthBlockCipherText) - - // (2) decrypt remainder of outer encryption blocks (1 ... n-1'th) using - // the decrypted nthBlock as the IV - outerRemainderDecrypter := cipher.NewCBCDecrypter(kekDesCipher, decryptedNthBlock) - - decryptedOuterRemainder := make([]byte, encryptedCEKLen-1*blockByteSize) - outerRemainderDecrypter.CryptBlocks(decryptedOuterRemainder, encryptedCEK[:encryptedCEKLen-1*blockByteSize]) - - // combine decrypted remainder with decrypted nth block for complete decrypted bytes - // this is equivelant to having the outer encryption removed, AKA the CEK is encrypted - // once now instead of twice - onceEncryptedCEK := append(decryptedOuterRemainder, decryptedNthBlock...) - - // (3) Decrypted the inner layer of encryption using the KEK (aka decrypt the remaining - // layer of encryption) - - // inner decrypter uses original CEK salt - innerDecrypter := cipher.NewCBCDecrypter(kekDesCipher, encryptedCekSalt) - - // once decrypted, the CEK is still formatted as: - // CEK byte count || check value || CEK || padding (if required) - formattedCEK := make([]byte, len(onceEncryptedCEK)) - innerDecrypter.CryptBlocks(formattedCEK, onceEncryptedCEK) - - // Now that CEK is decrypted, sanity check it - - // first byte is CEK byte count - expectedCEKLen := formattedCEK[0] - - // (1a) expected cek len must be 16 or 24 or 3DES (which is what APC uses) - if int(expectedCEKLen) != 16 && int(expectedCEKLen) != 24 { - return nil, errors.New("expected CEK len block size is %d but 3DES requires 16 or 24 (decrypting likely failed)") - } - - // next 3 bytes are the check value - CEKCheckVal := formattedCEK[1:4] - - // CEK itself is the next bytes until CEK is the expected length - CEK = formattedCEK[4 : expectedCEKLen+4] - - // (1b) key check data validation - if !isBitwiseCompliment(CEKCheckVal, CEK[0:3]) { - return nil, errors.New("CEK check value did not match CEK") - } - - return CEK, nil -} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..94e0688 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,7 @@ +package main + +import "apc-p15-tool/pkg/app" + +func main() { + app.Start() +} diff --git a/go.mod b/go.mod index 1870494..e7eae35 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,21 @@ -module temp +module apc-p15-tool go 1.21 -require github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 +require ( + github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 + github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 + go.uber.org/zap v1.26.0 +) -require golang.org/x/crypto v0.18.0 +require go.uber.org/multierr v1.11.0 // indirect + +replace apc-p15-tool/cmd => /cmd + +replace apc-p15-tool/pkg/app => /pkg/app + +replace apc-p15-tool/pkg/pkcs15 => /pkg/pkcs15 + +replace apc-p15-tool/pkg/tools => /pkg/tools + +replace apc-p15-tool/pkg/tools/asn1obj => /pkg/tools/asn1obj diff --git a/go.sum b/go.sum index ccdf90c..da5b6be 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,22 @@ +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/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 h1:aiqS8aBlF9PsAKeMddMSfbwp3smONCn3UO8QfUg0Z7Y= +github.com/peterbourgon/ff/v4 v4.0.0-alpha.4/go.mod h1:H/13DK46DKXy7EaIxPhk2Y0EC8aubKm35nBjBe8AAGc= +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/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kek.go b/kek.go deleted file mode 100644 index fc29d9e..0000000 --- a/kek.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "crypto/sha256" - - "golang.org/x/crypto/pbkdf2" -) - -// makeKEK creates the APC KEK for a given Salt; APC uses a fixed -// password, iteration count, and hash function -func makeKEK(salt []byte) (KEK []byte) { - // password is known constant for APC files - password := "user" - - // fixed values for APC files - iterations := 5000 - hash := sha256.New - - // size of 3DES key (k1 + k2 + k3) - size := 24 - - // kek - return pbkdf2.Key([]byte(password), salt, iterations, size, hash) -} diff --git a/main.go b/main.go deleted file mode 100644 index 21f0009..0000000 --- a/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "log" - "os" -) - -func main() { - p15Bytes, err := os.ReadFile("./apc9138a8cert-no-header.p15") - if err != nil { - panic(err) - } - - apcHeader, err := makeFileHeader(p15Bytes) - if err != nil { - panic(err) - } - - wizardBytes, err := os.ReadFile("./apc9138a.apc-wizard.p15") - if err != nil { - panic(err) - } - - wizHeader := wizardBytes[:228] - - log.Println(apcHeader) - log.Println(wizHeader) - - for i := range wizHeader { - if apcHeader[i] != wizHeader[i] { - panic(i) - } - } - - log.Println("match") - -} diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..2261258 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,65 @@ +package app + +import ( + "apc-p15-tool/pkg/pkcs15" + "encoding/base64" + "os" + + "go.uber.org/zap" +) + +// struct for receivers to use common app pieces +type app struct { + logger *zap.SugaredLogger + config *config +} + +// actual application start +func Start() { + // make app w/ initial logger pre-config + initLogLevel := "debug" + app := &app{ + logger: makeZapLogger(&initLogLevel), + } + + // get config + app.getConfig() + + // re-init logger with configured log level + app.logger = makeZapLogger(app.config.logLevel) + + // break point for building additional alternate functions + + // function: make p15 from pem files + + // Read in PEM files + keyPem, err := os.ReadFile(*app.config.keyPemFilePath) + if err != nil { + app.logger.Fatalf("failed to read key file (%s)", err) + // FATAL + } + + certPem, err := os.ReadFile(*app.config.certPemFilePath) + if err != nil { + app.logger.Fatalf("failed to read cert file (%s)", err) + // FATAL + } + + p15, err := pkcs15.ParsePEMToPKCS15(keyPem, certPem) + if err != nil { + app.logger.Fatalf("failed to parse pem files (%s)", err) + // FATAL + } + + // TEMP TEMP TEMP + p15File, err := p15.ToP15File() + if err != nil { + app.logger.Fatalf("failed to make p15 file (%s)", err) + // FATAL + } + + // app.logger.Debug(hex.EncodeToString(p15File)) + app.logger.Debug(base64.RawStdEncoding.EncodeToString(p15File)) + + // TEMP TEMP TEMP +} diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go new file mode 100644 index 0000000..9edbc3e --- /dev/null +++ b/pkg/app/app_config.go @@ -0,0 +1,45 @@ +package app + +import ( + "os" + + "github.com/peterbourgon/ff/v4" + "github.com/peterbourgon/ff/v4/ffhelp" +) + +const ( + environmentVarPrefix = "APC_P15_TOOL" +) + +// app's config options from user +type config struct { + logLevel *string + keyPemFilePath *string + certPemFilePath *string +} + +// getConfig returns the app's configuration from either command line args, +// or environment variables +func (app *app) getConfig() { + // make config and flag set + cfg := &config{} + fs := ff.NewFlagSet("apc-p15-tool") + + // define options + cfg.logLevel = fs.StringEnum('l', "loglevel", "log level: debug, info, warn, error, dpanic, panic, or fatal", + "info", "debug", "warn", "error", "dpanic", "panic", "fatal") + + cfg.keyPemFilePath = fs.StringLong("keyfile", "", "path and filename of the rsa-2048 key in pem format") + cfg.certPemFilePath = fs.StringLong("certfile", "", "path and filename of the rsa-2048 key in pem format") + // TODO key and pem directly in a flag/env var + + // parse using args and/or ENV vars + err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix(environmentVarPrefix)) + if err != nil { + app.logger.Fatal(ffhelp.Flags(fs)) + // FATAL + } + + // set app config + app.config = cfg +} diff --git a/pkg/app/app_logger.go b/pkg/app/app_logger.go new file mode 100644 index 0000000..1846079 --- /dev/null +++ b/pkg/app/app_logger.go @@ -0,0 +1,47 @@ +package app + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// makeZapLogger creates a logger for the app; if log level is nil or does not parse +// the default 'Info' level will be used. +func makeZapLogger(logLevel *string) *zap.SugaredLogger { + // default info level + zapLevel := zapcore.InfoLevel + var parseErr error + + // try to parse specified level (if there is one) + if logLevel != nil { + parseLevel, err := zapcore.ParseLevel(*logLevel) + if err != nil { + parseErr = err + } else { + zapLevel = parseLevel + } + } + + // make zap config + config := zap.NewProductionEncoderConfig() + config.EncodeTime = zapcore.ISO8601TimeEncoder + config.LineEnding = "\n" + + // no stack trace + config.StacktraceKey = "" + + // make logger + consoleEncoder := zapcore.NewConsoleEncoder(config) + core := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zapLevel) + + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)).Sugar() + + // log deferred parse error if there was one + if logLevel != nil && parseErr != nil { + logger.Errorf("failed to parse requested log level \"%s\" (%s)", *logLevel, parseErr) + } + + return logger +} diff --git a/file_header.go b/pkg/app/file_header.go similarity index 94% rename from file_header.go rename to pkg/app/file_header.go index eef3550..b3f365a 100644 --- a/file_header.go +++ b/pkg/app/file_header.go @@ -1,4 +1,4 @@ -package main +package app import ( "encoding/binary" @@ -24,7 +24,7 @@ func makeFileHeader(p15File []byte) ([]byte, error) { // *(uint32_t *)(buf + 220) = (int32_t)calc_cksum(0, buf + 228, fileSize); // checksum of the original file // *(uint32_t *)(buf + 224) = (int32_t)calc_cksum(0, buf, 224); // checksum of the APC header - // NOTE: This line is unused as it seems the APC tool code always writes this as a 1 for 2,048 bit + // NOTE: This line is unused as it seems the APC CLI tool v1.0.0 code always writes this as a 1 (regardless of key length) // *(uint32_t *)(buf + 208) = keySize; // 1 for 1024 key, otherwise (2048 bit) 2 // Unsure why this was in original code but seems irrelevant diff --git a/pkg/pkcs15/keyid.go b/pkg/pkcs15/keyid.go new file mode 100644 index 0000000..3eb14f2 --- /dev/null +++ b/pkg/pkcs15/keyid.go @@ -0,0 +1,35 @@ +package pkcs15 + +import ( + "apc-p15-tool/pkg/tools/asn1obj" + "crypto/sha1" + "math/big" +) + +// keyId returns the keyId for the overall key object +func (p15 *pkcs15KeyCert) keyId() []byte { + // Create Object to hash + hashObj := asn1obj.Sequence([][]byte{ + asn1obj.Sequence([][]byte{ + // Key is RSA + asn1obj.ObjectIdentifier(asn1obj.OIDrsaEncryptionPKCS1), + asn1obj.Null(), + }), + // BIT STRING of rsa key public key + asn1obj.BitString( + asn1obj.Sequence([][]byte{ + asn1obj.Integer(p15.key.N), + asn1obj.Integer((big.NewInt(int64(p15.key.E)))), + }), + ), + }) + + // SHA-1 Hash + hasher := sha1.New() + _, err := hasher.Write(hashObj) + if err != nil { + panic(err) + } + + return hasher.Sum(nil) +} diff --git a/pkg/pkcs15/marshal.go b/pkg/pkcs15/marshal.go new file mode 100644 index 0000000..b2eeaf1 --- /dev/null +++ b/pkg/pkcs15/marshal.go @@ -0,0 +1,66 @@ +package pkcs15 + +import ( + "apc-p15-tool/pkg/tools/asn1obj" + "math/big" +) + +const ( + apcKeyLabel = "Private key" +) + +// ToP15File turns the key and cert into a properly formatted and encoded +// p15 file +func (p15 *pkcs15KeyCert) ToP15File() ([]byte, error) { + // private key object + pkey, err := p15.toP15PrivateKey() + if err != nil { + return nil, err + } + + // ContentInfo + p15File := asn1obj.Sequence([][]byte{ + + // contentType: OID: 1.2.840.113549.1.15.3.1 pkcs15content (PKCS #15 content type) + asn1obj.ObjectIdentifier(asn1obj.OIDPkscs15Content), + + // content + asn1obj.Explicit(0, + asn1obj.Sequence([][]byte{ + asn1obj.Integer(big.NewInt(0)), + asn1obj.Sequence([][]byte{ + asn1obj.Explicit(0, + asn1obj.Explicit(0, + pkey, + ), + ), + }), + }), + ), + }) + + return p15File, nil +} + +// toP15PrivateKey creates the encoded private key. it is broken our from the larger p15 +// function for readability +func (p15 *pkcs15KeyCert) toP15PrivateKey() ([]byte, error) { + // key object + key := asn1obj.Sequence([][]byte{ + // commonObjectAttributes - Label + asn1obj.Sequence([][]byte{ + asn1obj.UTF8String(apcKeyLabel), + }), + // CommonKeyAttributes + asn1obj.Sequence([][]byte{ + // CommonKeyAttributes - iD - uses keyId that is SHA1( SubjectPublicKeyInfo SEQUENCE ) + asn1obj.OctetString(p15.keyId()), + // CommonKeyAttributes - usage (trailing 0s will drop) + asn1obj.BitString([]byte{byte(0b11100010)}), + // CommonKeyAttributes - accessFlags (trailing 0s will drop) + asn1obj.BitString([]byte{byte(0b10110000)}), + }), + }) + + return key, nil +} diff --git a/pkg/pkcs15/oids.go b/pkg/pkcs15/oids.go new file mode 100644 index 0000000..bec89d7 --- /dev/null +++ b/pkg/pkcs15/oids.go @@ -0,0 +1 @@ +package pkcs15 diff --git a/pkg/pkcs15/pem_decode.go b/pkg/pkcs15/pem_decode.go new file mode 100644 index 0000000..7fa4a6b --- /dev/null +++ b/pkg/pkcs15/pem_decode.go @@ -0,0 +1,120 @@ +package pkcs15 + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +var ( + errPemKeyBadBlock = errors.New("pkcs15: pem key: failed to decode pem block") + errPemKeyFailedToParse = errors.New("pkcs15: pem key: failed to parse key") + errPemKeyWrongBlockType = errors.New("pkcs15: pem key: unsupported pem block type (only pkcs1 and pkcs8 supported)") + errPemKeyWrongType = errors.New("pkcs15: pem key: unsupported key type (only rsa 2,048 supported)") + + errPemCertBadBlock = errors.New("pkcs15: pem cert: failed to decode pem block") + errPemCertFailedToParse = errors.New("pkcs15: pem cert: failed to parse cert") +) + +// pemKeyDecode attempts to decode a pem encoded byte slice and then attempts +// to parse an RSA private key from the decoded pem block. an error is returned +// if any of these steps fail OR if the rsa key is not of bitlen 2,048 +func pemKeyDecode(keyPem []byte) (*rsa.PrivateKey, error) { + // decode + pemBlock, _ := pem.Decode([]byte(keyPem)) + if pemBlock == nil { + return nil, errPemKeyBadBlock + } + + // parsing depends on block type + var rsaKey *rsa.PrivateKey + + switch pemBlock.Type { + case "RSA PRIVATE KEY": // PKCS1 + var err error + + rsaKey, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes) + if err != nil { + return nil, errPemKeyFailedToParse + } + + // basic sanity check + err = rsaKey.Validate() + if err != nil { + return nil, fmt.Errorf("pkcs15: pem key: failed sanity check (%s)", err) + } + + // verify proper bitlen + if rsaKey.N.BitLen() != 2048 { + return nil, errPemKeyWrongType + } + + // good to go + + case "PRIVATE KEY": // PKCS8 + pkcs8Key, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) + if err != nil { + return nil, errPemKeyFailedToParse + } + + switch pkcs8Key := pkcs8Key.(type) { + case *rsa.PrivateKey: + rsaKey = pkcs8Key + + // basic sanity check + err = rsaKey.Validate() + if err != nil { + return nil, fmt.Errorf("pkcs15: pem key: failed sanity check (%s)", err) + } + + // verify proper bitlen + if rsaKey.N.BitLen() != 2048 { + return nil, errPemKeyWrongType + } + + // good to go + + default: + return nil, errPemKeyWrongType + } + + default: + return nil, errPemKeyWrongBlockType + } + + // if rsaKey is nil somehow, error + if rsaKey == nil { + return nil, errors.New("pkcs15: pem key: rsa key unexpectedly nil (report bug to project repo)") + } + + // success! + return rsaKey, nil +} + +// pemCertDecode attempts to decode a pem encoded byte slice and then attempts +// to parse a certificate from it. The certificate is also check against the +// key that is passed in to verify the key matches the certificate. +func pemCertDecode(certPem, keyPem []byte) (*x509.Certificate, error) { + // verify key and cert make a valid key pair + _, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return nil, err + } + + // discard rest, apc tool only bundles end cert + block, _ := pem.Decode(certPem) + if block == nil || block.Type != "CERTIFICATE" { + return nil, errPemCertBadBlock + } + + // Get the cert struct + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errPemCertFailedToParse + } + + return cert, nil +} diff --git a/pkg/pkcs15/pem_to_p15.go b/pkg/pkcs15/pem_to_p15.go new file mode 100644 index 0000000..dcd899a --- /dev/null +++ b/pkg/pkcs15/pem_to_p15.go @@ -0,0 +1,36 @@ +package pkcs15 + +import ( + "crypto/rsa" + "crypto/x509" +) + +// pkcs15KeyCert holds the data for a key and certificate pair; it provides +// various methods to transform pkcs15 data +type pkcs15KeyCert struct { + key *rsa.PrivateKey + cert *x509.Certificate +} + +// ParsePEMToPKCS15 parses the provide pem files to a pkcs15 struct; it also does some +// basic sanity check; if any of this fails, an error is returned +func ParsePEMToPKCS15(keyPem, certPem []byte) (*pkcs15KeyCert, error) { + // decode / check key + key, err := pemKeyDecode(keyPem) + if err != nil { + return nil, err + } + + // decode / check cert + cert, err := pemCertDecode(certPem, keyPem) + if err != nil { + return nil, err + } + + p15 := &pkcs15KeyCert{ + key: key, + cert: cert, + } + + return p15, nil +} diff --git a/pkg/tools/asn1obj/bitstring.go b/pkg/tools/asn1obj/bitstring.go new file mode 100644 index 0000000..a7840e6 --- /dev/null +++ b/pkg/tools/asn1obj/bitstring.go @@ -0,0 +1,27 @@ +package asn1obj + +import ( + "encoding/asn1" + "math/bits" +) + +// BitString returns a BIT STRING of the content +func BitString(content []byte) []byte { + bs := asn1.BitString{ + Bytes: content, + } + + // drop trailing 0s by removing them from overall length + if len(content) > 0 { + trailing0s := bits.TrailingZeros8(content[len(content)-1]) + bs.BitLength = 8*len(content) - trailing0s + } + + // should never error + asn1result, err := asn1.Marshal(bs) + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/pkg/tools/asn1obj/integer.go b/pkg/tools/asn1obj/integer.go new file mode 100644 index 0000000..6aef433 --- /dev/null +++ b/pkg/tools/asn1obj/integer.go @@ -0,0 +1,17 @@ +package asn1obj + +import ( + "encoding/asn1" + "math/big" +) + +// Integer returns an ASN.1 OBJECT IDENTIFIER with the oidValue bytes +func Integer(bigInt *big.Int) []byte { + // should never error + asn1result, err := asn1.Marshal(bigInt) + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/pkg/tools/asn1obj/misc.go b/pkg/tools/asn1obj/misc.go new file mode 100644 index 0000000..75bbc66 --- /dev/null +++ b/pkg/tools/asn1obj/misc.go @@ -0,0 +1,27 @@ +package asn1obj + +import "encoding/asn1" + +// Explicit wraps another ASN.1 Object with the EXPLICIT wrapper using +// the tag number specified +func Explicit(explicitTagNumber int, wrappedElement []byte) []byte { + raw := asn1.RawValue{ + Class: asn1.ClassContextSpecific, + Tag: explicitTagNumber, + IsCompound: true, + Bytes: wrappedElement, + } + + // should never error + asn1result, err := asn1.Marshal(raw) + if err != nil { + panic(err) + } + + return asn1result +} + +// Null returns the NULL value +func Null() []byte { + return asn1.NullBytes +} diff --git a/pkg/tools/asn1obj/octetstring.go b/pkg/tools/asn1obj/octetstring.go new file mode 100644 index 0000000..989070d --- /dev/null +++ b/pkg/tools/asn1obj/octetstring.go @@ -0,0 +1,23 @@ +package asn1obj + +import ( + "encoding/asn1" +) + +// OctetString returns an OCTET STRING of the content +func OctetString(content []byte) []byte { + raw := asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOctetString, + IsCompound: false, + Bytes: content, + } + + // should never error + asn1result, err := asn1.Marshal(raw) + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/pkg/tools/asn1obj/oid.go b/pkg/tools/asn1obj/oid.go new file mode 100644 index 0000000..8327919 --- /dev/null +++ b/pkg/tools/asn1obj/oid.go @@ -0,0 +1,19 @@ +package asn1obj + +import "encoding/asn1" + +var ( + OIDPkscs15Content = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 15, 3, 1} // pkcs15content (PKCS #15 content type) + OIDrsaEncryptionPKCS1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} // rsaEncryption (PKCS #1) +) + +// ObjectIdentifier returns an ASN.1 OBJECT IDENTIFIER with the oidValue bytes +func ObjectIdentifier(oid asn1.ObjectIdentifier) []byte { + // should never error + asn1result, err := asn1.Marshal(oid) + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/pkg/tools/asn1obj/sequence.go b/pkg/tools/asn1obj/sequence.go new file mode 100644 index 0000000..1dc88e5 --- /dev/null +++ b/pkg/tools/asn1obj/sequence.go @@ -0,0 +1,26 @@ +package asn1obj + +import "encoding/asn1" + +// Sequence returns an ASN.1 SEQUENCE with the specified content +func Sequence(content [][]byte) []byte { + val := []byte{} + for i := range content { + val = append(val, content[i]...) + } + + raw := asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagSequence, + IsCompound: true, + Bytes: val, + } + + // should never error + asn1result, err := asn1.Marshal(raw) + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/pkg/tools/asn1obj/utf8string.go b/pkg/tools/asn1obj/utf8string.go new file mode 100644 index 0000000..6937340 --- /dev/null +++ b/pkg/tools/asn1obj/utf8string.go @@ -0,0 +1,16 @@ +package asn1obj + +import ( + "encoding/asn1" +) + +// UTF8String returns the specified string as a UTF8String +func UTF8String(s string) []byte { + // should never error + asn1result, err := asn1.MarshalWithParams(s, "utf8") + if err != nil { + panic(err) + } + + return asn1result +} diff --git a/bitwise.go b/pkg/tools/bitwise.go similarity index 68% rename from bitwise.go rename to pkg/tools/bitwise.go index ad43680..0fb70fc 100644 --- a/bitwise.go +++ b/pkg/tools/bitwise.go @@ -1,7 +1,7 @@ -package main +package tools -// bitwiseComplimentOf returns the bitwise compliment of data -func bitwiseComplimentOf(data []byte) []byte { +// BitwiseComplimentOf returns the bitwise compliment of data +func BitwiseComplimentOf(data []byte) []byte { compliment := []byte{} for i := range data { @@ -11,9 +11,9 @@ func bitwiseComplimentOf(data []byte) []byte { return compliment } -// isBitwiseCompliment returns true if data1 and data2 are bitwise compliments, +// IsBitwiseCompliment returns true if data1 and data2 are bitwise compliments, // otherwise it returns false -func isBitwiseCompliment(data1, data2 []byte) bool { +func IsBitwiseCompliment(data1, data2 []byte) bool { // if not same length, definitely not compliments if len(data1) != len(data2) { return false