diff --git a/pkg/app/cmd_install.go b/pkg/app/cmd_install.go index eacda53..d94c00c 100644 --- a/pkg/app/cmd_install.go +++ b/pkg/app/cmd_install.go @@ -145,7 +145,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error { // verify cert is the correct one certVerified := bytes.Equal(leafCert.Raw, pemBlock.Bytes) if !certVerified { - return errors.New("install: web ui leaf cert does not match new cert") + return errors.New("install: web ui leaf cert does not match new cert (your cert may not be compatible with NMC; check for WARNINGs in this tool's output)") } app.stdLogger.Println("install: ups web ui cert verified") diff --git a/pkg/app/pem_to_p15.go b/pkg/app/pem_to_p15.go index d48d4a8..c878abb 100644 --- a/pkg/app/pem_to_p15.go +++ b/pkg/app/pem_to_p15.go @@ -2,8 +2,11 @@ package app import ( "apc-p15-tool/pkg/pkcs15" + "crypto/x509" + "encoding/asn1" "fmt" "slices" + "time" ) // list of keys supported by the NMC2 @@ -13,6 +16,32 @@ var nmc2SupportedKeyTypes = []pkcs15.KeyType{ pkcs15.KeyTypeRSA3072, // officially not supported but works } +// known good signing algorithms +var knownSupportedNMC2SigningAlgs = []x509.SignatureAlgorithm{ + x509.SHA256WithRSA, +} + +var knownSupportedNMC3SigningAlgs = append(knownSupportedNMC2SigningAlgs, []x509.SignatureAlgorithm{ + x509.ECDSAWithSHA384, +}...) + +// known supported cert extensions +var knownSupportedCriticalOIDs = []asn1.ObjectIdentifier{ + {2, 5, 29, 15}, // keyUsage + {2, 5, 29, 19}, // basicConstraints + {2, 5, 29, 17}, // subjectAltName +} + +var knownSupportedOIDs = append(knownSupportedCriticalOIDs, []asn1.ObjectIdentifier{ + {2, 5, 29, 37}, // extKeyUsage + {2, 5, 29, 14}, // subjectKeyIdentifier + {2, 5, 29, 35}, // authorityKeyIdentifier + {1, 3, 6, 1, 5, 5, 7, 1, 1}, // authorityInfoAccess + {2, 5, 29, 32}, // certificatePolicies + {1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}, // googleSignedCertificateTimestamp + {2, 5, 29, 31}, // cRLDistributionPoints +}...) + // pemToAPCP15 reads the specified pem files and returns the apc p15 file(s). If the // key type of the key is not supported by NMC2, the combined key+cert file is not // generated and nil is returned instead for that file. If the key IS supported by @@ -37,7 +66,10 @@ func (app *app) pemToAPCP15(keyPem, certPem []byte, parentCmdName string) (keyFi app.stdLogger.Printf("%s: successfully generated p15 key file content", parentCmdName) // check key type for compat with NMC2 + nmc2KeyType := false if slices.Contains(nmc2SupportedKeyTypes, p15.KeyType()) { + nmc2KeyType = true + app.stdLogger.Printf("%s: key type is supported by NMC2, generating p15 key+cert file content...", parentCmdName) // make file bytes @@ -54,11 +86,87 @@ func (app *app) pemToAPCP15(keyPem, certPem []byte, parentCmdName string) (keyFi // combine header with file apcKeyCertFile = append(apcHeader, keyCertFile...) - } else { - // NMC2 unsupported - app.stdLogger.Printf("%s: key type is not supported by NMC2, skipping p15 key+cert file content", parentCmdName) } + // check various parts of cert and log compatibility warnings + warned := false + + // key not supported for NMC2 + if !nmc2KeyType { + app.stdLogger.Printf("WARNING: %s: key type is %s and is not supported by NMC2.", parentCmdName, p15.KeyType().String()) + warned = true + } + + // signature algorithm (see: https://github.com/gregtwallace/apc-p15-tool/issues/18) + if !nmc2KeyType { + // definitely not for NMC2 + if !slices.Contains(knownSupportedNMC3SigningAlgs, p15.Cert.SignatureAlgorithm) { + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and it is not known if NMC3 supports this algorithm.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + warned = true + } + } else { + // could be for either NMC2 or NMC3 + if !slices.Contains(knownSupportedNMC2SigningAlgs, p15.Cert.SignatureAlgorithm) { + if !slices.Contains(knownSupportedNMC3SigningAlgs, p15.Cert.SignatureAlgorithm) { + // not in NMC2 or NMC3 list + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and is not supported by NMC2. It is also not known if NMC3 supports this algorithm.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + } else { + // not in NMC2 list, but is in NMC3 list + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and it does not support NMC2.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + } + warned = true + } + } + + // if support by 2, check 2 list + // if not found on 2 list, check 3 list + + // check validity dates + if time.Now().Before(p15.Cert.NotBefore) { + app.stdLogger.Printf("WARNING: %s: Current time (%s) is before certificate's NotBefore time (%s).", + parentCmdName, time.Now().Format(timeLoggingFormat), p15.Cert.NotBefore.Format(timeLoggingFormat)) + warned = true + } + + if time.Now().After(p15.Cert.NotAfter) { + app.stdLogger.Printf("WARNING: %s: Current time (%s) is after certificate's NotAfter time (%s).", + parentCmdName, time.Now().Format(timeLoggingFormat), p15.Cert.NotAfter.Format(timeLoggingFormat)) + warned = true + } + + // check extensions against known working extensions + for _, extension := range p15.Cert.Extensions { + // critical or not? + okOIDs := knownSupportedCriticalOIDs + criticalLogMsg := "Critical " + if !extension.Critical { + okOIDs = knownSupportedOIDs + criticalLogMsg = "" + } + + // validate OIDs + ok := false + for _, okOID := range okOIDs { + if okOID.Equal(extension.Id) { + ok = true + break + } + } + + if !ok { + app.stdLogger.Printf("WARNING: %s: %sExtension %s may not be supported by NMC.", parentCmdName, criticalLogMsg, extension.Id.String()) + } + } + + // log a message about possible failure + if warned { + app.stdLogger.Printf("WARNING: %s: Possible certificate compatibility issues were detected. If the resulting p15 file "+ + "does not work with your NMC (e.g., a self-signed certificate is regenerated after you try to install the p15), "+ + "modify your certificate to resolve the warnings and try again.", parentCmdName) + } + + // end compatibility warnings + app.stdLogger.Printf("%s: apc p15 file(s) data succesfully generated", parentCmdName) return keyFile, apcKeyCertFile, nil diff --git a/pkg/pkcs15/keyid.go b/pkg/pkcs15/keyid.go index 08a3ce4..cac7301 100644 --- a/pkg/pkcs15/keyid.go +++ b/pkg/pkcs15/keyid.go @@ -15,7 +15,7 @@ func (p15 *pkcs15KeyCert) keyId() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawSubjectPublicKeyInfo) + _, err := hasher.Write(p15.Cert.RawSubjectPublicKeyInfo) if err != nil { panic(err) } @@ -46,9 +46,9 @@ func (p15 *pkcs15KeyCert) keyIdInt3() []byte { // object to hash hashObj := asn1obj.Sequence([][]byte{ // issuerDistinguishedName - p15.cert.RawIssuer, + p15.Cert.RawIssuer, // serialNumber - asn1obj.Integer(p15.cert.SerialNumber), + asn1obj.Integer(p15.Cert.SerialNumber), }) // SHA-1 Hash @@ -74,7 +74,7 @@ func (p15 *pkcs15KeyCert) keyIdInt6() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawIssuer) + _, err := hasher.Write(p15.Cert.RawIssuer) if err != nil { panic(err) } @@ -95,7 +95,7 @@ func (p15 *pkcs15KeyCert) keyIdInt7() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawSubject) + _, err := hasher.Write(p15.Cert.RawSubject) if err != nil { panic(err) } @@ -168,7 +168,7 @@ func (p15 *pkcs15KeyCert) keyIdInt9() []byte { // to be ~ 1 hour ish BEFORE the cert was even created. Key would also // obviously have to be created prior to the cert creation. time := make([]byte, 4) - binary.BigEndian.PutUint32(time, uint32(p15.cert.NotBefore.Unix())) + binary.BigEndian.PutUint32(time, uint32(p15.Cert.NotBefore.Unix())) publicKeyPacket = append(publicKeyPacket, time...) // the next part is key type specific diff --git a/pkg/pkcs15/pem_parse.go b/pkg/pkcs15/pem_parse.go index 19e44f1..9df0ecd 100644 --- a/pkg/pkcs15/pem_parse.go +++ b/pkg/pkcs15/pem_parse.go @@ -10,8 +10,8 @@ import ( // pkcs15KeyCert holds the data for a key and certificate pair; it provides // various methods to transform pkcs15 data type pkcs15KeyCert struct { + Cert *x509.Certificate key crypto.PrivateKey - cert *x509.Certificate // store the encrypted enveloped Private Key for re-use envelopedPrivateKey []byte } @@ -32,6 +32,31 @@ const ( KeyTypeUnknown ) +// String returns the private key type in a log friendly string format. +func (keyType KeyType) String() string { + switch keyType { + case KeyTypeRSA1024: + return "RSA 1024-bit" + case KeyTypeRSA2048: + return "RSA 2048-bit" + case KeyTypeRSA3072: + return "RSA 3072-bit" + case KeyTypeRSA4096: + return "RSA 4096-bit" + + case KeyTypeECP256: + return "ECDSA P-256" + case KeyTypeECP384: + return "ECDSA P-384" + case KeyTypeECP521: + return "ECDSA P-521" + + default: + } + + return "unknown key type" +} + // KeyType returns the private key type func (p15 *pkcs15KeyCert) KeyType() KeyType { switch pKey := p15.key.(type) { @@ -85,7 +110,7 @@ func ParsePEMToPKCS15(keyPem, certPem []byte) (*pkcs15KeyCert, error) { // create p15 struct p15 := &pkcs15KeyCert{ key: key, - cert: cert, + Cert: cert, } // pre-calculate encrypted envelope diff --git a/pkg/pkcs15/pem_to_p15.go b/pkg/pkcs15/pem_to_p15.go index 0c2214d..a68aba2 100644 --- a/pkg/pkcs15/pem_to_p15.go +++ b/pkg/pkcs15/pem_to_p15.go @@ -43,9 +43,9 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { // CommonKeyAttributes - accessFlags (trailing 0s will drop) asn1obj.BitString([]byte{byte(0b10110000)}), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [0] endDate - asn1obj.GeneralizedTimeExplicitValue(0, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(0, p15.Cert.NotAfter), }), // ObjectValue - indirect-protected asn1obj.ExplicitCompound(1, [][]byte{ @@ -74,9 +74,9 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { // CommonKeyAttributes - accessFlags (trailing 0s will drop) asn1obj.BitString([]byte{byte(0b10110000)}), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [0] endDate - asn1obj.GeneralizedTimeExplicitValue(0, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(0, p15.Cert.NotAfter), }), // ObjectValue - indirect-protected asn1obj.ExplicitCompound(1, [][]byte{ @@ -114,15 +114,15 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { p15.keyIdInt9(), }), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [4] endDate - asn1obj.GeneralizedTimeExplicitValue(4, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(4, p15.Cert.NotAfter), }), // actual certificate itself asn1obj.ExplicitCompound(1, [][]byte{ asn1obj.Sequence([][]byte{ asn1obj.ExplicitCompound(0, [][]byte{ - p15.cert.Raw, + p15.Cert.Raw, }), }), }),