Skip to content

Commit

Permalink
Create self signed chain from command line (#1)
Browse files Browse the repository at this point in the history
* Create test chain from command line

Add functionality to create a test chain from the commandline
Refactor things:
- Cert chain always in fixed order
- `*[]x509.Certificate` -> `[]*x509.Certificate`
- `*[][]byte` -> `[][]byte`

* Prevented infinite loop

- Multiple CA roots co-exist when the -t flag is enabled.

* Remove debug print statements from findOtherNameValue

Removed two debug print statements that printed extension IDs and other name types. These statements were cluttering the output and are not necessary for the final implementation.

* Refactor certificate validation to support test and self-signed CAs

Introduce `allowUziTestCa` and `allowSelfSignedCa` flags to UraValidatorImpl for more flexible certificate chain validation. Refactor the validation logic to handle these new flags appropriately, ensuring better support for various CA configurations.

* Fix certificate lookup in validator

Re-enable the use of findSigningCertificate to ensure proper certificate validation based on X509 thumbprint. Remove hardcoding of the signingCert as first certificate in the chain and handle potential errors appropriately.

* Update test-cert CLI to include UZI, URA, and AGB parameters

Improved the test-cert command to specify UZI, URA, and AGB parameters instead of a single identifier. This change enhances flexibility and allows for a more precise definition of test certificates. Updated help text for better clarity and added comments to explain the format of the otherName field.

* Handle file write errors separately

Previously, errors when writing "chain.pem" and "signing_key.pem" were not handled separately. This change ensures that each file write operation checks for errors independently and exits with an error message if a write fails.

* Rename variables and use helper function for root CA check

Updated `FormatDid` function to use a more descriptive variable `caCert` instead of `ca`. Modified the `FindRootCertificate` function to use the `x509_cert.IsRootCa` helper function for improved readability and consistency in the root CA check.

* Bugfix: Rename and update CertTemplate to include organization. The Name seems to be the field that identifies the signer and subject.

Updated CertTemplate function to accept an additional "organization" parameter, which allows different organization names for certificates. Adjusted related calls and modified default certificate values to reflect these changes.

* Add `subject_did` field and update `test-cert` case

Introduce a new `subject_did` field in the `TestCert` struct with a default value. Update the `test-cert` command case to handle this new parameter and adjust the VC issuance logic.

* Potentially unsafe quoting: Refactor JSON unmarshal logic in Validate method

Replaced the string formatting approach with direct JSON marshaling to prevent potentially unsafe quoting.

* Refactor variable name in EncodeCertificates function

Renamed the loop variable from 'cert' to 'c' to prevent a naming collision with the cert import.

* Add a comment to keep DebugUnmarshall for future debugging

A comment was added to the DebugUnmarshall method to indicate that it should be retained for future debugging purposes.

---------

Co-authored-by: Roland Groen <[email protected]>
  • Loading branch information
stevenvegt and rolandgroen authored Oct 15, 2024
1 parent ef8021f commit abe731e
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 239 deletions.
22 changes: 11 additions & 11 deletions ca_certs/uzi_ca_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ func GetCertPools(includeTest bool) (root *x509.CertPool, intermediate *x509.Cer
return downloadUziPool(pool)
}

func GetCerts(includeTest bool) (*[]x509.Certificate, error) {
func GetCerts(includeTest bool) ([]*x509.Certificate, error) {
pool := prepareAndCombinePools(includeTest)
return downloadUziPoolCerts(pool)
}

func GetDERs(includeTest bool) (*[][]byte, error) {
func GetDERs(includeTest bool) ([][]byte, error) {
pool := prepareAndCombinePools(includeTest)
return downloadUziPoolDERs(pool)
}
Expand All @@ -50,19 +50,19 @@ func prepareAndCombinePools(includeTest bool) UziCaPool {
return pool
}

func downloadUziPoolDERs(pool UziCaPool) (*[][]byte, error) {
func downloadUziPoolDERs(pool UziCaPool) ([][]byte, error) {
var rv = [][]byte{}
certs, err := downloadUziPoolCerts(pool)
if err != nil {
return nil, err
}
for _, cert := range *certs {
for _, cert := range certs {
rv = append(rv, cert.Raw)
}
return &rv, err
return rv, err
}

func GetTestCerts() (*[]x509.Certificate, error) {
func GetTestCerts() ([]*x509.Certificate, error) {
return downloadUziPoolCerts(TestUziCaPool)
}

Expand All @@ -81,7 +81,7 @@ func downloadUziPool(pool UziCaPool) (*x509.CertPool, *x509.CertPool, error) {
return roots, intermediates, nil
}

func downloadUziPoolCerts(pool UziCaPool) (*[]x509.Certificate, error) {
func downloadUziPoolCerts(pool UziCaPool) ([]*x509.Certificate, error) {
allUrls := append(pool.rootCaUrls, pool.intermediateCaUrls...)
all, err := downloadCerts(allUrls)
if err != nil {
Expand All @@ -103,16 +103,16 @@ func downloadPool(urls []string) (*x509.CertPool, error) {
return roots, nil
}

func downloadCerts(urls []string) (*[]x509.Certificate, error) {
certs := make([]x509.Certificate, 0)
func downloadCerts(urls []string) ([]*x509.Certificate, error) {
certs := make([]*x509.Certificate, 0)
for _, url := range urls {
certificate, err := readCertificateFromUrl(url)
if err != nil {
return nil, err
}
certs = append(certs, *certificate)
certs = append(certs, certificate)
}
return &certs, nil
return certs, nil
}

func readCertificateFromUrl(url string) (*x509.Certificate, error) {
Expand Down
26 changes: 9 additions & 17 deletions did_x509/did_x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,9 @@ type X509Did struct {

// FormatDid constructs a decentralized identifier (DID) from a certificate chain and an optional policy.
// It returns the formatted DID string or an error if the root certificate or hash calculation fails.
func FormatDid(chain *[]x509.Certificate, policy string) (string, error) {
root, err := FindRootCertificate(chain)
if err != nil {
return "", err
}
func FormatDid(caCert *x509.Certificate, policy string) (string, error) {
alg := "sha512"
rootHash, err := x509_cert.Hash(root.Raw, alg)
rootHash, err := x509_cert.Hash(caCert.Raw, alg)
if err != nil {
return "", err
}
Expand All @@ -42,17 +38,13 @@ func FormatDid(chain *[]x509.Certificate, policy string) (string, error) {
// CreateDid generates a Decentralized Identifier (DID) from a given certificate chain.
// It extracts the Unique Registration Address (URA) from the chain, creates a policy with it, and formats the DID.
// Returns the generated DID or an error if any step fails.
func CreateDid(chain *[]x509.Certificate) (string, error) {
certificate, _, err := x509_cert.FindSigningCertificate(chain)
if err != nil || certificate == nil {
return "", err
}
otherNameValue, sanType, err := x509_cert.FindOtherName(certificate)
func CreateDid(signingCert, caCert *x509.Certificate) (string, error) {
otherNameValue, sanType, err := x509_cert.FindOtherName(signingCert)
if err != nil {
return "", err
}
policy := CreatePolicy(otherNameValue, sanType)
formattedDid, err := FormatDid(chain, policy)
formattedDid, err := FormatDid(caCert, policy)
return formattedDid, err
}
func ParseDid(didString string) (*X509Did, error) {
Expand Down Expand Up @@ -83,10 +75,10 @@ func CreatePolicy(otherNameValue string, sanType x509_cert.SanTypeName) string {
}

// FindRootCertificate traverses a chain of x509 certificates and returns the first certificate that is a CA.
func FindRootCertificate(chain *[]x509.Certificate) (*x509.Certificate, error) {
for _, cert := range *chain {
if cert.IsCA {
return &cert, nil
func FindRootCertificate(chain []*x509.Certificate) (*x509.Certificate, error) {
for _, cert := range chain {
if x509_cert.IsRootCa(cert) {
return cert, nil
}
}
return nil, fmt.Errorf("cannot find root certificate")
Expand Down
46 changes: 42 additions & 4 deletions did_x509/did_x509_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 2 additions & 18 deletions did_x509/did_x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestDefaultDidCreator_CreateDid(t *testing.T) {
type fields struct {
}
type args struct {
chain *[]x509.Certificate
chain []*x509.Certificate
}
chain, _, rootCert, _, _, err := x509_cert.BuildCertChain("A_BIG_STRING")
if err != nil {
Expand All @@ -34,22 +34,6 @@ func TestDefaultDidCreator_CreateDid(t *testing.T) {
want string
errMsg string
}{
{
name: "Test case 1",
fields: fields{},
args: args{chain: &[]x509.Certificate{}},
want: "",
errMsg: "no certificates provided",
},
{
name: "Test case 2",
fields: fields{},
args: args{chain: &[]x509.Certificate{
{},
}},
want: "",
errMsg: "no certificate found in the SAN attributes, please check if the certificate is an UZI Server Certificate",
},
{
name: "Happy path",
fields: fields{},
Expand All @@ -60,7 +44,7 @@ func TestDefaultDidCreator_CreateDid(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateDid(tt.args.chain)
got, err := CreateDid(tt.args.chain[0], tt.args.chain[len(tt.args.chain)-1])
wantErr := tt.errMsg != ""
if (err != nil) != wantErr {
t.Errorf("DefaultDidProcessor.CreateDid() error = %v, errMsg %v", err, tt.errMsg)
Expand Down
81 changes: 71 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ import (
"fmt"
"github.com/alecthomas/kong"
"github.com/nuts-foundation/uzi-did-x509-issuer/uzi_vc_issuer"
"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
"os"
)

type VC struct {
CertificateFile string `arg:"" name:"certificate_file" help:"Certificate PEM file." type:"existingfile"`
CertificateFile string `arg:"" name:"certificate_file" help:"Certificate PEM file. If the file contains a chain, the chain will be used for signing." type:"existingfile"`
SigningKey string `arg:"" name:"signing_key" help:"PEM key for signing." type:"existingfile"`
SubjectDID string `arg:"" name:"subject_did" help:"The subject DID of the VC." type:"key"`
Test bool `short:"t" help:"Allow test certificates."`
Test bool `short:"t" help:"Allow for certificates signed by the TEST UZI Root CA."`
}

type TestCert struct {
Uzi string `arg:"" name:"uzi" help:"The UZI number for the test certificate."`
Ura string `arg:"" name:"ura" help:"The URA number for the test certificate."`
Agb string `arg:"" name:"agb" help:"The AGB code for the test certificate."`
SubjectDID string `arg:"" default:"did:web:example.com:test" name:"subject_did" help:"The subject DID of the VC." type:"key"`
}

var CLI struct {
Version string `help:"Show version."`
Vc VC `cmd:"" help:"Create a new VC."`
Version string `help:"Show version."`
Vc VC `cmd:"" help:"Create a new VC."`
TestCert TestCert `cmd:"" help:"Create a new test certificate."`
}

func main() {
Expand All @@ -25,17 +34,69 @@ func main() {
if err != nil {
panic(err)
}
_, err = parser.Parse(os.Args[1:])
ctx, err := parser.Parse(os.Args[1:])
if err != nil {
parser.FatalIfErrorf(err)
}
vc := cli.Vc
jwt, err := issueVc(vc)
if err != nil {
fmt.Println(err)

switch ctx.Command() {
case "vc <certificate_file> <signing_key> <subject_did>":
vc := cli.Vc
jwt, err := issueVc(vc)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
println(jwt)
case "test-cert <uzi> <ura> <agb>", "test-cert <uzi> <ura> <agb> <subject_did>":
// Format is 2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344
// <OID CA>-<versie-nr>-<UZI-nr>-<pastype>-<Abonnee-nr>-<rol>-<AGB-code>
// 2.16.528.1.1007.99.2110-1-<UZI-nr>-S-<Abonnee-nr>-00.000-<AGB-code>
otherName := fmt.Sprintf("2.16.528.1.1007.99.2110-1-%s-S-%s-00.000-%s", cli.TestCert.Uzi, cli.TestCert.Ura, cli.TestCert.Agb)
fmt.Println("Building certificate chain for identifier:", otherName)
chain, _, _, privKey, _, err := x509_cert.BuildCertChain(otherName)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}

chainPems, err := x509_cert.EncodeCertificates(chain...)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
signingKeyPem, err := x509_cert.EncodeRSAPrivateKey(privKey)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}

err = os.WriteFile("chain.pem", chainPems, 0644)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
err = os.WriteFile("signing_key.pem", signingKeyPem, 0644)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
vc := VC{
CertificateFile: "chain.pem",
SigningKey: "signing_key.pem",
SubjectDID: cli.TestCert.SubjectDID,
Test: false,
}
jwt, err := issueVc(vc)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
println(jwt)
default:
fmt.Println("Unknown command")
os.Exit(-1)
}
println(jwt)
}

func issueVc(vc VC) (string, error) {
Expand Down
18 changes: 10 additions & 8 deletions pem/pem_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package pem

import (
"encoding/pem"
"fmt"
"os"
)

// ParseFileOrPath processes a file or directory at the given path and extracts PEM blocks of the specified pemType.
func ParseFileOrPath(path string, pemType string) (*[][]byte, error) {
func ParseFileOrPath(path string, pemType string) ([][]byte, error) {
fileInfo, err := os.Stat(path)
if err != nil {
return nil, err
Expand All @@ -25,9 +26,9 @@ func ParseFileOrPath(path string, pemType string) (*[][]byte, error) {
if err != nil {
return nil, err
}
files = append(files, *blocks...)
files = append(files, blocks...)
}
return &files, nil
return files, nil
} else {
blocks, err := readFile(path, pemType)
return blocks, err
Expand All @@ -36,21 +37,22 @@ func ParseFileOrPath(path string, pemType string) (*[][]byte, error) {
}

// readFile reads a file from the given filename, parses it for PEM blocks of the specified type, and returns the blocks.
func readFile(filename string, pemType string) (*[][]byte, error) {
func readFile(filename string, pemType string) ([][]byte, error) {
fmt.Println("filename: ", filename)
files := make([][]byte, 0)
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
if looksLineCert(content, pemType) {
foundBlocks := ParsePemBlocks(content, pemType)
files = append(files, *foundBlocks...)
files = append(files, foundBlocks...)
}
return &files, nil
return files, nil
}

// ParsePemBlocks extracts specified PEM blocks from the provided certificate bytes and returns them as a pointer to a slice of byte slices.
func ParsePemBlocks(cert []byte, pemType string) *[][]byte {
func ParsePemBlocks(cert []byte, pemType string) [][]byte {
blocks := make([][]byte, 0)
for {
pemBlock, tail := pem.Decode(cert)
Expand All @@ -66,7 +68,7 @@ func ParsePemBlocks(cert []byte, pemType string) *[][]byte {
cert = tail

}
return &blocks
return blocks
}

// looksLineCert checks if the given certificate data is a valid PEM block of the specified type.
Expand Down
Loading

0 comments on commit abe731e

Please sign in to comment.