kairoaraujo / goca

Golang Certificate Authority (CA) package
MIT License
38 stars 14 forks source link

Segmentation violation when attempting to load intermediate CA #8

Closed necheffa closed 2 years ago

necheffa commented 2 years ago

When issuing an intermediate CA, the RSA key as well as the CSR are placed in the "ca" directory for the intermediate's common name, however the .crt file containing the signed certificate is placed under the root CA's "certs" directory (e.g. /mypki/myRoot/certs/myIntermediate.crt). Later, when attempting to goca.Load() the intermediate CA, goca expects the .crt file to be under /mypki/myIntermediate/ca/myIntermediate.crt. Because of this, in loadCa() the caData.Certificate field ends up being empty and later on this causes a segmentation fault.

A temporary work around is to symlink to the .crt so later calls to Load() find the .crt where it is expected.

A design decision will need to be made in order to create a fix. Either CA.SignCSR() will need to be smart enough to realize it is signing an intermediate CA and handle the location of the resulting .crt specially; some variation of goca.New() special handling for intermediate CAs / a new function call for issuing intermediate CAs; or goca.Load() needs to be smart enough to realize it is handling an intermediate CA and lookup the .crt in the signer's certs dir (seems hacky and gross IMO).

There is probably an opportunity for stronger "file exists" style validation in CA.loadCa() too since it relies on the existence of a directory matching the specified common name and assumes the child files of that directory exist.

See the following snippet for a demonstration of the issue.

package main

import (
    "os"
    "log"
    "fmt"

    "github.com/kairoaraujo/goca"
)

func main() {
    os.Setenv("CAPATH", "./crypt/mypki")

    // make sure root CA and intermediate CA generation
    // happen separately so when we go to sign a server
    // cert with the intermediate we have to Load()
    // the intermediate from disk.
    One()
    Two()
}

func Two() {
    // NOTE: crashes at the Load call below.
    interCA, err := goca.Load("myIntermediate")
    if err != nil {
        fmt.Println("loading inter")
        log.Fatal(err)
    }

    srvID := goca.Identity{
        Organization: "Something",
        OrganizationalUnit: "Something",
        Country: "US",
        Locality: "Somewhere",
        Province: "Some State",
        Intermediate: false,
    }

    _, err = interCA.IssueCertificate("yolo", srvID)
    if err != nil {
        fmt.Println("signing srv w/ inter")
        log.Fatal(err)
    }
}

func One() {
    rootCAID := goca.Identity{
        Organization: "Some Org.",
        OrganizationalUnit: "Certificate Authority",
        Country:    "US",
        Locality:   "Somewhere",
        Province:   "Some State",
        Intermediate:   false,
        KeyBitSize: 4096,
        // although regular certs have a max validity of 825 days, there does not
        // appear to be a max imposed by goca for root CAs.
        Valid:      3650,
    }

    rootCA, err := goca.New("myRoot", rootCAID)

    if err != nil {
        fmt.Println("create root CA")
        log.Fatal(err)
    }

    interID := goca.Identity{
        Organization: "Some Org.",
        OrganizationalUnit: "Intermediate",
        Country: "US",
        Locality: "Somewhere",
        Province: "Some State",
        Intermediate: true,
        KeyBitSize: 4096,
        // with goca, intermediates are also limited to 825 days, probably for the best
        Valid: 825,
    }

    interCA, err := goca.New("myIntermediate", interID)
    if err != nil {
        fmt.Println("create inter CA")
        log.Fatal(err)
    }

    if !interCA.IsIntermediate() {
        fmt.Println("inter is not inter...")
    }

    // When Intermediate == true, goca only generates a CSR from New()
    // and the root CA needs to sign it to seal the deal and make it a
    // proper intermediate CA.
    _, err = rootCA.SignCSR(*interCA.GoCSR(), 825)
    if err != nil {
        fmt.Println("sign inter with root")
        log.Fatal(err)
    }
}
necheffa commented 2 years ago

@kairoaraujo Wanted to get your thoughts on the viability of my proposed solution before trying to implement it:

I think the best way to handle this is in CA.SignCSR(), if we set up the paths/storage type right here it seems like the rest of the code should "just work" as intended.

The SignCSR method can take a third boolean indicating if we are signing a regular certificate or another CA.

With a variadic argument signature, we can preserve the existing two-argument signature and it will just default to signing regular certificates by calling the new three-argument form with the appropriate boolean to indicate a regular cert.

On the REST API side, similarly add an optional boolean field in the JSON, if it is there call the three-argument SignCSR passing in the provided setting. Otherwise just call the two-argument SignCSR that defaults to signing regular certs.