diff --git a/pkg/app/cmd_create.go b/pkg/app/cmd_create.go index 51cf1cc..bc9ecdf 100644 --- a/pkg/app/cmd_create.go +++ b/pkg/app/cmd_create.go @@ -6,7 +6,10 @@ import ( "os" ) -const createDefaultOutFilePath = "apctool.p15" +const ( + createDefaultOutFilePath = "apctool.p15" + createDefaultOutKeyFilePath = "apctool.key.p15" +) // cmdCreate is the app's command to create an apc p15 file from key and cert // pem files @@ -26,25 +29,35 @@ func (app *app) cmdCreate(_ context.Context, args []string) error { // validation done - // make p15 file - apcFile, err := app.pemToAPCP15(keyPem, certPem, "create") + // make p15 files + apcKeyCertFile, keyFile, err := app.pemToAPCP15s(keyPem, certPem, "create") if err != nil { return err } // determine file name (should already be done by flag parsing, but avoid nil just in case) - fileName := createDefaultOutFilePath + keyCertFileName := createDefaultOutFilePath if app.config.create.outFilePath != nil && *app.config.create.outFilePath != "" { - fileName = *app.config.create.outFilePath + keyCertFileName = *app.config.create.outFilePath } - // write file - err = os.WriteFile(fileName, apcFile, 0777) + keyFileName := createDefaultOutFilePath + if app.config.create.outKeyFilePath != nil && *app.config.create.outKeyFilePath != "" { + keyFileName = *app.config.create.outKeyFilePath + } + + // write files + err = os.WriteFile(keyCertFileName, apcKeyCertFile, 0777) if err != nil { - return fmt.Errorf("create: failed to write apc p15 file (%s)", err) + return fmt.Errorf("create: failed to write apc p15 key+cert file (%s)", err) } + app.stdLogger.Printf("create: apc p15 key+cert file %s written to disk", keyCertFileName) - app.stdLogger.Printf("create: apc p15 file %s written to disk", fileName) + err = os.WriteFile(keyFileName, keyFile, 0777) + if err != nil { + return fmt.Errorf("create: failed to write apc p15 key file (%s)", err) + } + app.stdLogger.Printf("create: apc p15 key file %s written to disk", keyFileName) return nil } diff --git a/pkg/app/cmd_install.go b/pkg/app/cmd_install.go index 5d826ec..26635e7 100644 --- a/pkg/app/cmd_install.go +++ b/pkg/app/cmd_install.go @@ -52,7 +52,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error { // validation done // make p15 file - apcFile, err := app.pemToAPCP15(keyPem, certPem, "install") + apcFile, _, err := app.pemToAPCP15s(keyPem, certPem, "install") if err != nil { return err } diff --git a/pkg/app/config.go b/pkg/app/config.go index 6514391..bea67aa 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -28,7 +28,8 @@ type config struct { debugLogging *bool create struct { keyCertPemCfg - outFilePath *string + outFilePath *string + outKeyFilePath *string } install struct { keyCertPemCfg @@ -71,7 +72,8 @@ func (app *app) getConfig(args []string) error { cfg.create.certPemFilePath = createFlags.StringLong("certfile", "", "path and filename of the certificate in pem format") cfg.create.keyPem = createFlags.StringLong("keypem", "", "string of the rsa-1024 or rsa-2048 key in pem format") cfg.create.certPem = createFlags.StringLong("certpem", "", "string of the certificate in pem format") - cfg.create.outFilePath = createFlags.StringLong("outfile", createDefaultOutFilePath, "path and filename to write the p15 file to") + cfg.create.outFilePath = createFlags.StringLong("outfile", createDefaultOutFilePath, "path and filename to write the key+cert p15 file to") + cfg.create.outKeyFilePath = createFlags.StringLong("outkeyfile", createDefaultOutKeyFilePath, "path and filename to write the key p15 file to") createCmd := &ff.Command{ Name: "create", diff --git a/pkg/app/file_header.go b/pkg/app/file_header.go index b803d68..20097cd 100644 --- a/pkg/app/file_header.go +++ b/pkg/app/file_header.go @@ -7,6 +7,8 @@ import ( "github.com/sigurn/crc16" ) +const apcHeaderLen = 228 + // makeFileHeader generates the 228 byte header to prepend to the .p15 // as required by APC UPS NMC. Contrary to the apc_tools repo, it does // mot appear the header changes based on key size. @@ -28,7 +30,7 @@ func makeFileHeader(p15File []byte) ([]byte, error) { // *(uint32_t *)(buf + 208) = keySize; // 1 for 1024 key, otherwise (2048 bit) 2 // Unsure why this was in original code but seems irrelevant - header := make([]byte, 228) + header := make([]byte, apcHeaderLen) // always 1 header[0] = 1 diff --git a/pkg/app/pem_to_p15.go b/pkg/app/pem_to_p15.go index eb020aa..b006c9e 100644 --- a/pkg/app/pem_to_p15.go +++ b/pkg/app/pem_to_p15.go @@ -5,34 +5,36 @@ import ( "fmt" ) -// pemToAPCP15 reads the specified pem files and returns the apc p15 bytes -func (app *app) pemToAPCP15(keyPem, certPem []byte, parentCmdName string) ([]byte, error) { +// pemToAPCP15s reads the specified pem files and returns the apc p15 files (both a +// p15 file with just the private key, and also a p15 file with both the private key +// and certificate). The key+cert file includes the required APC header, prepended. +func (app *app) pemToAPCP15s(keyPem, certPem []byte, parentCmdName string) (apcKeyCertFile, keyFile []byte, err error) { app.stdLogger.Printf("%s: making apc p15 file from pem", parentCmdName) // make p15 struct p15, err := pkcs15.ParsePEMToPKCS15(keyPem, certPem) if err != nil { - return nil, fmt.Errorf("%s: failed to parse pem files (%w)", parentCmdName, err) + return nil, nil, fmt.Errorf("%s: failed to parse pem files (%w)", parentCmdName, err) } app.stdLogger.Printf("%s: successfully loaded pem files", parentCmdName) // make file bytes - p15File, err := p15.ToP15File() + keyCertFile, keyFile, err := p15.ToP15Files() if err != nil { - return nil, fmt.Errorf("%s: failed to make p15 file (%w)", parentCmdName, err) + return nil, nil, fmt.Errorf("%s: failed to make p15 file (%w)", parentCmdName, err) } // make header for file bytes - apcHeader, err := makeFileHeader(p15File) + apcHeader, err := makeFileHeader(keyCertFile) if err != nil { - return nil, fmt.Errorf("%s: failed to make p15 file header (%w)", parentCmdName, err) + return nil, nil, fmt.Errorf("%s: failed to make p15 file header (%w)", parentCmdName, err) } // combine header with file - apcFile := append(apcHeader, p15File...) + apcKeyCertFile = append(apcHeader, keyCertFile...) app.stdLogger.Printf("%s: apc p15 file data succesfully generated", parentCmdName) - return apcFile, nil + return apcKeyCertFile, keyFile, nil } diff --git a/pkg/pkcs15/pem_to_p15.go b/pkg/pkcs15/pem_to_p15.go index a7a9fef..19392f2 100644 --- a/pkg/pkcs15/pem_to_p15.go +++ b/pkg/pkcs15/pem_to_p15.go @@ -2,6 +2,7 @@ package pkcs15 import ( "apc-p15-tool/pkg/tools/asn1obj" + "encoding/asn1" "math/big" ) @@ -9,62 +10,11 @@ const ( apcKeyLabel = "Private key" ) -// ToP15File turns the key and cert into a properly formatted and encoded -// p15 file -func (p15 *pkcs15KeyCert) ToP15File() ([]byte, error) { +// toP15KeyCert creates a P15 file with both the private key and certificate, mirroring the +// final p15 file an APC UPS expects (though without the header) +func (p15 *pkcs15KeyCert) toP15KeyCert(keyEnvelope []byte) (keyCert []byte, err error) { // private key object - pkey, err := p15.toP15PrivateKey() - if err != nil { - return nil, err - } - - cert, err := p15.toP15Cert() - 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.ExplicitCompound(0, [][]byte{ - asn1obj.Sequence([][]byte{ - asn1obj.Integer(big.NewInt(0)), - asn1obj.Sequence([][]byte{ - asn1obj.ExplicitCompound(0, [][]byte{ - asn1obj.ExplicitCompound(0, [][]byte{ - pkey, - }), - }), - asn1obj.ExplicitCompound(4, [][]byte{ - asn1obj.ExplicitCompound(0, [][]byte{ - cert, - }), - }), - }), - }), - }), - }) - - return p15File, nil -} - -// toP15PrivateKey creates the encoded private key. it is broken our from the larger p15 -// function for readability -// NOTE: Do not use this to try and turn just a private key into a p15, the format isn't -// quite the same. -func (p15 *pkcs15KeyCert) toP15PrivateKey() ([]byte, error) { - // rsa encrypted key in encrypted envelope - envelope, err := p15.encryptedKeyEnvelope() - if err != nil { - return nil, err - } - - // key object - key := asn1obj.Sequence([][]byte{ + privateKey := asn1obj.Sequence([][]byte{ // commonObjectAttributes - Label asn1obj.Sequence([][]byte{ asn1obj.UTF8String(apcKeyLabel), @@ -87,20 +37,12 @@ func (p15 *pkcs15KeyCert) toP15PrivateKey() ([]byte, error) { asn1obj.Sequence([][]byte{ // AuthEnvelopedData Type ([4]) asn1obj.ExplicitCompound(4, [][]byte{ - envelope, + keyEnvelope, }), }), }), }) - return key, nil -} - -// toP15Cert creates the encoded certificate. it is broken our from the larger p15 -// function for readability -// NOTE: Do not use this to try and turn just a cert into a p15. I don't believe, -// such a thing is permissible under the spec. -func (p15 *pkcs15KeyCert) toP15Cert() ([]byte, error) { // cert object cert := asn1obj.Sequence([][]byte{ // commonObjectAttributes - Label @@ -134,5 +76,163 @@ func (p15 *pkcs15KeyCert) toP15Cert() ([]byte, error) { }), }) - return cert, nil + // build the file + + // ContentInfo + keyCert = asn1obj.Sequence([][]byte{ + + // contentType: OID: 1.2.840.113549.1.15.3.1 pkcs15content (PKCS #15 content type) + asn1obj.ObjectIdentifier(asn1obj.OIDPkscs15Content), + + // content + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.Sequence([][]byte{ + asn1obj.Integer(big.NewInt(0)), + asn1obj.Sequence([][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + privateKey, + }), + }), + asn1obj.ExplicitCompound(4, [][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + cert, + }), + }), + }), + }), + }), + }) + + return keyCert, nil +} + +// toP15Key creates a P15 file with just the private key, mirroring the p15 format +// the APC tool uses when generating a new private key (Note: no header is used on +// this file) +func (p15 *pkcs15KeyCert) toP15Key(keyEnvelope []byte) (key []byte, err error) { + // private key object (slightly different than the key+cert format) + privateKey := 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)}), + }), + + // + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.Sequence([][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + p15.keyIdInt2(), + p15.keyIdInt8(), + p15.keyIdInt9(), + }), + }), + }), + + // ObjectValue - indirect-protected + asn1obj.ExplicitCompound(1, [][]byte{ + asn1obj.Sequence([][]byte{ + // AuthEnvelopedData Type ([4]) + asn1obj.ExplicitCompound(4, [][]byte{ + keyEnvelope, + }), + }), + }), + }) + + // ContentInfo + key = asn1obj.Sequence([][]byte{ + + // contentType: OID: 1.2.840.113549.1.15.3.1 pkcs15content (PKCS #15 content type) + asn1obj.ObjectIdentifier(asn1obj.OIDPkscs15Content), + + // content + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.Sequence([][]byte{ + asn1obj.Integer(big.NewInt(0)), + asn1obj.Sequence([][]byte{ + // [0] Private Key + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + privateKey, + }), + }), + // [1] Public Key + asn1obj.ExplicitCompound(1, [][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.Sequence([][]byte{ + // commonObjectAttributes - Label + asn1obj.Sequence([][]byte{ + asn1obj.UTF8String(apcKeyLabel), + }), + // CommonKeyAttributes + asn1obj.Sequence([][]byte{ + asn1obj.OctetString(p15.keyId()), + asn1obj.BitString([]byte{byte(0b10000010)}), + asn1obj.BitString([]byte{byte(0b01000000)}), + }), + + asn1obj.ExplicitCompound(1, [][]byte{ + asn1obj.Sequence([][]byte{ + asn1obj.ExplicitCompound(0, [][]byte{ + asn1obj.ExplicitCompound(1, [][]byte{ + asn1obj.Sequence([][]byte{ + asn1obj.ObjectIdentifier(asn1obj.OIDrsaEncryptionPKCS1), + asn1.NullBytes, + }), + // RSAPublicKey SubjectPublicKeyInfo + asn1obj.BitString( + asn1obj.Sequence([][]byte{ + asn1obj.Integer(p15.key.PublicKey.N), + asn1obj.Integer(big.NewInt(int64(p15.key.PublicKey.E))), + }), + ), + }), + }), + // not 100% certain but appears to be rsa key byte len + asn1obj.Integer(big.NewInt(int64(p15.key.PublicKey.N.BitLen() / 8))), + }), + }), + }), + }), + }), + }), + }), + }), + }) + + return key, nil +} + +// ToP15File turns the key and cert into a properly formatted and encoded +// p15 file +func (p15 *pkcs15KeyCert) ToP15Files() (keyCertFile []byte, keyFile []byte, err error) { + // rsa encrypted key in encrypted envelope (will be shared by both output files) + envelope, err := p15.encryptedKeyEnvelope() + if err != nil { + return nil, nil, err + } + + // key + cert file + keyCertFile, err = p15.toP15KeyCert(envelope) + if err != nil { + return nil, nil, err + } + + // key only file + keyFile, err = p15.toP15Key(envelope) + if err != nil { + return nil, nil, err + } + + return keyCertFile, keyFile, nil }