From 2535bc78eb5895e79ff169197dcce2fe1420408a Mon Sep 17 00:00:00 2001 From: stevenvegt Date: Mon, 14 Oct 2024 13:27:28 +0200 Subject: [PATCH 01/13] 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` --- ca_certs/uzi_ca_certs.go | 22 ++--- did_x509/did_x509.go | 26 ++--- did_x509/did_x509_mock.go | 46 ++++++++- did_x509/did_x509_test.go | 20 +--- main.go | 55 +++++++++-- pem/pem_reader.go | 18 ++-- pem/pem_reader_test.go | 14 +-- uzi_vc_issuer/ura_issuer.go | 156 ++++++++++++++++++------------ uzi_vc_issuer/ura_issuer_test.go | 10 +- uzi_vc_validator/ura_validator.go | 77 ++++++++------- x509_cert/x509_cert.go | 24 +++-- x509_cert/x509_cert_mock.go | 12 +-- x509_cert/x509_cert_test.go | 10 +- x509_cert/x509_test_utils.go | 67 ++++++++----- x509_cert/x509_utils.go | 16 +-- 15 files changed, 351 insertions(+), 222 deletions(-) diff --git a/ca_certs/uzi_ca_certs.go b/ca_certs/uzi_ca_certs.go index a1cdc9a..7e6a913 100644 --- a/ca_certs/uzi_ca_certs.go +++ b/ca_certs/uzi_ca_certs.go @@ -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) } @@ -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) } @@ -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 { @@ -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) { diff --git a/did_x509/did_x509.go b/did_x509/did_x509.go index f4d1701..39380c3 100644 --- a/did_x509/did_x509.go +++ b/did_x509/did_x509.go @@ -22,7 +22,7 @@ type X509Did struct { // DidCreator is an interface for creating a DID (Decentralized Identifier) given a chain of x509 certificates. // The CreateDid method takes a slice of x509.Certificate and returns a DID as a string and an error if any. type DidCreator interface { - CreateDid(chain *[]x509.Certificate) (string, error) + CreateDid(signingCert, caCert *x509.Certificate) (string, error) } type DidParser interface { @@ -43,13 +43,9 @@ func NewDidParser() *DefaultDidProcessor { // 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(ca *x509.Certificate, policy string) (string, error) { alg := "sha512" - rootHash, err := x509_cert.Hash(root.Raw, alg) + rootHash, err := x509_cert.Hash(ca.Raw, alg) if err != nil { return "", err } @@ -64,17 +60,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 (d *DefaultDidProcessor) 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 (d *DefaultDidProcessor) 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 (d *DefaultDidProcessor) ParseDid(didString string) (*X509Did, error) { @@ -105,10 +97,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 { +func FindRootCertificate(chain []*x509.Certificate) (*x509.Certificate, error) { + for _, cert := range chain { if cert.IsCA { - return &cert, nil + return cert, nil } } return nil, fmt.Errorf("cannot find root certificate") diff --git a/did_x509/did_x509_mock.go b/did_x509/did_x509_mock.go index e0cd1e6..99dece3 100644 --- a/did_x509/did_x509_mock.go +++ b/did_x509/did_x509_mock.go @@ -40,16 +40,54 @@ func (m *MockDidCreator) EXPECT() *MockDidCreatorMockRecorder { } // CreateDid mocks base method. -func (m *MockDidCreator) CreateDid(chain *[]x509.Certificate) (string, error) { +func (m *MockDidCreator) CreateDid(signingCert, caCert *x509.Certificate) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateDid", chain) + ret := m.ctrl.Call(m, "CreateDid", signingCert, caCert) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateDid indicates an expected call of CreateDid. -func (mr *MockDidCreatorMockRecorder) CreateDid(chain any) *gomock.Call { +func (mr *MockDidCreatorMockRecorder) CreateDid(signingCert, caCert any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDid", reflect.TypeOf((*MockDidCreator)(nil).CreateDid), chain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDid", reflect.TypeOf((*MockDidCreator)(nil).CreateDid), signingCert, caCert) +} + +// MockDidParser is a mock of DidParser interface. +type MockDidParser struct { + ctrl *gomock.Controller + recorder *MockDidParserMockRecorder +} + +// MockDidParserMockRecorder is the mock recorder for MockDidParser. +type MockDidParserMockRecorder struct { + mock *MockDidParser +} + +// NewMockDidParser creates a new mock instance. +func NewMockDidParser(ctrl *gomock.Controller) *MockDidParser { + mock := &MockDidParser{ctrl: ctrl} + mock.recorder = &MockDidParserMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDidParser) EXPECT() *MockDidParserMockRecorder { + return m.recorder +} + +// ParseDid mocks base method. +func (m *MockDidParser) ParseDid(did string) (*X509Did, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseDid", did) + ret0, _ := ret[0].(*X509Did) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseDid indicates an expected call of ParseDid. +func (mr *MockDidParserMockRecorder) ParseDid(did any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseDid", reflect.TypeOf((*MockDidParser)(nil).ParseDid), did) } diff --git a/did_x509/did_x509_test.go b/did_x509/did_x509_test.go index 7e3747c..9ab6079 100644 --- a/did_x509/did_x509_test.go +++ b/did_x509/did_x509_test.go @@ -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 { @@ -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{}, @@ -61,7 +45,7 @@ func TestDefaultDidCreator_CreateDid(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := &DefaultDidProcessor{} - got, err := d.CreateDid(tt.args.chain) + got, err := d.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) diff --git a/main.go b/main.go index 1fc1b39..c449532 100644 --- a/main.go +++ b/main.go @@ -16,9 +16,14 @@ type VC struct { Test bool `short:"t" help:"Allow test certificates."` } +type TestCert struct { + Identifier string `arg:"" name:"identifier" help:"Identifier for the test certificate such as an URA or UZI number."` +} + 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() { @@ -27,17 +32,51 @@ 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 ": + vc := cli.Vc + jwt, err := issueVc(vc) + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + println(jwt) + case "test-cert ": + otherName := fmt.Sprintf("2.16.528.1.1007.1.%s", cli.TestCert.Identifier) + fmt.Println("Building certificate chain for identifier:", otherName) + chain, _, _, privKey, _, err := x509_cert.BuildCertChain(cli.TestCert.Identifier) + 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) + } + + os.WriteFile("chain.pem", chainPems, 0644) + os.WriteFile("signing_key.pem", signingKeyPem, 0644) + + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + default: + fmt.Println("Unknown command") os.Exit(-1) } - println(jwt) } func issueVc(vc VC) (string, error) { diff --git a/pem/pem_reader.go b/pem/pem_reader.go index f6c2e31..c2251a8 100644 --- a/pem/pem_reader.go +++ b/pem/pem_reader.go @@ -2,6 +2,7 @@ package pem import ( "encoding/pem" + "fmt" "os" ) @@ -20,7 +21,7 @@ func NewPemReader() *DefaultPemReader { } // ParseFileOrPath processes a file or directory at the given path and extracts PEM blocks of the specified pemType. -func (p *DefaultPemReader) ParseFileOrPath(path string, pemType string) (*[][]byte, error) { +func (p *DefaultPemReader) ParseFileOrPath(path string, pemType string) ([][]byte, error) { fileInfo, err := os.Stat(path) if err != nil { return nil, err @@ -39,9 +40,9 @@ func (p *DefaultPemReader) ParseFileOrPath(path string, pemType string) (*[][]by 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 @@ -50,7 +51,8 @@ func (p *DefaultPemReader) ParseFileOrPath(path string, pemType string) (*[][]by } // 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 { @@ -58,13 +60,13 @@ func readFile(filename string, pemType string) (*[][]byte, error) { } 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) @@ -80,7 +82,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. diff --git a/pem/pem_reader_test.go b/pem/pem_reader_test.go index d62c26b..dd63926 100644 --- a/pem/pem_reader_test.go +++ b/pem/pem_reader_test.go @@ -80,9 +80,9 @@ func TestParseFileOrPath(t *testing.T) { } data, err := pemReader.ParseFileOrPath(file.Name(), pemType) assert.NoError(t, err) - for i := 0; i < len(*data); i++ { - bytes := (*data)[i] - certificate := (*certs)[i] + for i := 0; i < len(data); i++ { + bytes := (data)[i] + certificate := (certs)[i] ok := assert.Equal(t, bytes, certificate.Raw) if !ok { t.Fail() @@ -116,14 +116,14 @@ func TestParseFileOrPath(t *testing.T) { data, err := pemReader.ParseFileOrPath(tempDir, pemType) assert.NoError(t, err) dataMap := make(map[string][]byte) - for i := 0; i < len(*data); i++ { - bytes := (*data)[i] + for i := 0; i < len(data); i++ { + bytes := (data)[i] hash, err := x509_cert.Hash(bytes, "sha512") assert.NoError(t, err) dataMap[base64.RawURLEncoding.EncodeToString(hash)] = bytes } - for i := 0; i < len(*certs); i++ { - bytes := (*certs)[i].Raw + for i := 0; i < len(certs); i++ { + bytes := (certs)[i].Raw hash, err := x509_cert.Hash(bytes, "sha512") assert.NoError(t, err) fileBytes := dataMap[base64.RawURLEncoding.EncodeToString(hash)] diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go index 81c91dd..500790b 100644 --- a/uzi_vc_issuer/ura_issuer.go +++ b/uzi_vc_issuer/ura_issuer.go @@ -10,21 +10,22 @@ import ( "encoding/pem" "errors" "fmt" + "regexp" + "time" + "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/cert" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/uzi-did-x509-issuer/ca_certs" "github.com/nuts-foundation/uzi-did-x509-issuer/did_x509" pem2 "github.com/nuts-foundation/uzi-did-x509-issuer/pem" "github.com/nuts-foundation/uzi-did-x509-issuer/uzi_vc_validator" "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" - "regexp" - "time" ) -import "github.com/nuts-foundation/go-did/vc" type UraIssuer interface { @@ -32,7 +33,10 @@ type UraIssuer interface { Issue(certificateFile string, signingKeyFile string, subjectDID string, subjectName string) (string, error) } -var RegexOtherNameValue = regexp.MustCompile(`2\.16\.528\.1\.1007.\d+\.\d+-\d+-\d+-S-(\d+)-00\.000-\d+`) +// RegexOtherNameValue matches thee OtherName field: ----- +// e.g.: 1-123456789-S-88888801-00.000-12345678 +// var RegexOtherNameValue = regexp.MustCompile(`2\.16\.528\.1\.1007.\d+\.\d+-\d+-\d+-S-(\d+)-00\.000-\d+`) +var RegexOtherNameValue = regexp.MustCompile(`\d+-\d+-S-(\d+)-00\.000-\d+`) // DefaultUraIssuer is responsible for building URA (UZI-register abonneenummer) Verifiable Credentials. // It utilizes a DidCreator to generate Decentralized Identifiers (DIDs) given a chain of x509 certificates. @@ -48,63 +52,61 @@ func NewUraVcBuilder(didCreator did_x509.DidCreator, chainParser x509_cert.Chain // Issue generates a URA Verifiable Credential using provided certificate, signing key, subject DID, and subject name. func (u DefaultUraIssuer) Issue(certificateFile string, signingKeyFile string, subjectDID string, test bool) (string, error) { - reader := pem2.NewPemReader() - certificate, err := reader.ParseFileOrPath(certificateFile, "CERTIFICATE") + pemReader := pem2.NewPemReader() + pemBlocks, err := pemReader.ParseFileOrPath(certificateFile, "CERTIFICATE") if err != nil { return "", err } - _certificates, err := u.chainParser.ParseCertificates(certificate) - if err != nil { - return "", err - } - if len(*_certificates) != 1 { - err = fmt.Errorf("did not find exactly one certificate in file %s", certificateFile) - return "", err - } - chain, err := ca_certs.GetDERs(test) - if err != nil { - return "", err + if len(pemBlocks) == 1 { + if !test { + err = fmt.Errorf("did not find exactly one certificate in file %s", certificateFile) + return "", err + } else { + certificate := pemBlocks[0] + pemBlocks, err = ca_certs.GetDERs(test) + if err != nil { + return "", err + } + pemBlocks = append(pemBlocks, certificate) + } } - _chain := append(*chain, *certificate...) - chain = &_chain - signingKeys, err := reader.ParseFileOrPath(signingKeyFile, "PRIVATE KEY") + signingKeys, err := pemReader.ParseFileOrPath(signingKeyFile, "PRIVATE KEY") if err != nil { return "", err } - if signingKeys == nil { + if len(signingKeys) == 0 { err := fmt.Errorf("no signing keys found") return "", err - } - var signingKey *[]byte - if len(*signingKeys) == 1 { - signingKey = &(*signingKeys)[0] - } else { - err := fmt.Errorf("no signing keys found") + privateKey, err := u.chainParser.ParsePrivateKey(signingKeys[0]) + if err != nil { return "", err } - privateKey, err := u.chainParser.ParsePrivateKey(signingKey) + + certs, err := u.chainParser.ParseCertificates(pemBlocks) if err != nil { return "", err } - certChain, err := u.chainParser.ParseCertificates(chain) + chain := BuildCertificateChain(certs) + err = validateChain(chain) if err != nil { + fmt.Println("error validating chain: ", err) return "", err } - credential, err := u.BuildUraVerifiableCredential(certChain, privateKey, subjectDID) + credential, err := u.BuildUraVerifiableCredential(chain, privateKey, subjectDID) if err != nil { return "", err } - marshal, err := json.Marshal(credential) + credentialJSON, err := json.Marshal(credential) if err != nil { return "", err } validator := uzi_vc_validator.NewUraValidator(did_x509.NewDidParser(), test) - jwtString := string(marshal) + jwtString := string(credentialJSON) jwtString = jwtString[1:] // Chop start jwtString = jwtString[:len(jwtString)-1] // Chop end err = validator.Validate(jwtString) @@ -115,25 +117,25 @@ func (u DefaultUraIssuer) Issue(certificateFile string, signingKeyFile string, s } // BuildUraVerifiableCredential constructs a verifiable credential with specified certificates, signing key, subject DID. -func (v DefaultUraIssuer) BuildUraVerifiableCredential(certificates *[]x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string) (*vc.VerifiableCredential, error) { - signingCert, otherNameValue, err := x509_cert.FindSigningCertificate(certificates) - if err != nil { - return nil, err - } - chain := BuildCertificateChain(certificates, signingCert) - err = validateChain(chain) - if err != nil { - return nil, err +func (v DefaultUraIssuer) BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string) (*vc.VerifiableCredential, error) { + if len(chain) == 0 { + return nil, errors.New("empty certificate chain") } - did, err := v.didCreator.CreateDid(chain) + did, err := v.didCreator.CreateDid(chain[0], chain[len(chain)-1]) if err != nil { return nil, err } + // signing cert is at the start of the chain + signingCert := chain[0] serialNumber := signingCert.Subject.SerialNumber if serialNumber == "" { return nil, errors.New("serialNumber not found in signing certificate ") } uzi := serialNumber + otherNameValue, _, err := x509_cert.FindOtherName(signingCert) + if err != nil { + return nil, err + } template, err := uraCredential(did, otherNameValue, uzi, subjectDID) if err != nil { return nil, err @@ -156,7 +158,7 @@ func (v DefaultUraIssuer) BuildUraVerifiableCredential(certificates *[]x509.Cert } // x5c - serializedCert, err := marshalChain(chain) + serializedCert, err := marshalChain(chain...) if err != nil { return "", err } @@ -183,9 +185,9 @@ func (v DefaultUraIssuer) BuildUraVerifiableCredential(certificates *[]x509.Cert // marshalChain converts a slice of x509.Certificate instances to a cert.Chain, encoding each certificate as PEM. // It returns the PEM-encoded cert.Chain and an error if the encoding or header fixation fails. -func marshalChain(certificates *[]x509.Certificate) (*cert.Chain, error) { +func marshalChain(certificates ...*x509.Certificate) (*cert.Chain, error) { chainPems := &cert.Chain{} - for _, certificate := range *certificates { + for _, certificate := range certificates { err := chainPems.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})) if err != nil { return nil, err @@ -195,43 +197,77 @@ func marshalChain(certificates *[]x509.Certificate) (*cert.Chain, error) { return headers, err } -func validateChain(certificates *[]x509.Certificate) error { - certs := *certificates +func validateChain(certs []*x509.Certificate) error { var prev *x509.Certificate = nil for i := range certs { certificate := certs[len(certs)-i-1] if prev != nil { - err := prev.CheckSignatureFrom(&certificate) + err := prev.CheckSignatureFrom(certificate) if err != nil { return err } } - if x509_cert.IsRootCa(&certificate) { + if x509_cert.IsRootCa(certificate) { return nil } - prev = &certificate + prev = certificate } return errors.New("failed to find a path to the root certificate in the chain, are you using a (Test) URA server certificate (Hint: the --test mode is required for Test URA server certificates)") } // BuildCertificateChain constructs a certificate chain from a given list of certificates and a starting signing certificate. // It recursively finds parent certificates for non-root CAs and appends them to the chain. -func BuildCertificateChain(certs *[]x509.Certificate, signingCert *x509.Certificate) *[]x509.Certificate { - var chain []x509.Certificate +// It assumes the list might not be in order. +// The returning chain contains the signing cert at the start and the root cert at the end. +func BuildCertificateChain(certs []*x509.Certificate) []*x509.Certificate { + var signingCert *x509.Certificate + for _, c := range certs { + if !c.IsCA { + signingCert = c + break + } + } if signingCert == nil { - return &chain + fmt.Println("failed to find signing certificate") + return nil } + + var chain []*x509.Certificate + chain = append(chain, signingCert) + + var root *x509.Certificate if !x509_cert.IsRootCa(signingCert) { - for _, parent := range *certs { - err := signingCert.CheckSignatureFrom(&parent) - if err == nil { - parentChain := BuildCertificateChain(certs, &parent) - chain = append(chain, *parentChain...) + // find the root certificate + for _, c := range certs { + if x509_cert.IsRootCa(c) { + root = c + break + } + } + + certToCheck := signingCert + for !certToCheck.Equal(root) { + found := false + for _, c := range certs { + if c.Equal(signingCert) { + continue + } + err := certToCheck.CheckSignatureFrom(c) + if err == nil { + + chain = append(chain, c) + certToCheck = c + found = true + break + } + } + if !found { + fmt.Println("failed to find path from signingCert to root") + return nil } } } - chain = append(chain, *signingCert) - return &chain + return chain } // convertClaims converts a map of claims to a JWT token. diff --git a/uzi_vc_issuer/ura_issuer_test.go b/uzi_vc_issuer/ura_issuer_test.go index acbdfeb..1a171e6 100644 --- a/uzi_vc_issuer/ura_issuer_test.go +++ b/uzi_vc_issuer/ura_issuer_test.go @@ -26,14 +26,14 @@ func TestBuildUraVerifiableCredential(t *testing.T) { tests := []struct { name string - in func() (*[]x509.Certificate, *rsa.PrivateKey, string) + in func() ([]*x509.Certificate, *rsa.PrivateKey, string) want func(error) bool }{ { name: "invalid signing certificate", - in: func() (*[]x509.Certificate, *rsa.PrivateKey, string) { - certs := []x509.Certificate{*cert} - return &certs, privKey, "did:example:123" + in: func() ([]*x509.Certificate, *rsa.PrivateKey, string) { + certs := []*x509.Certificate{cert} + return certs, privKey, "did:example:123" }, want: func(err error) bool { return err != nil @@ -44,6 +44,8 @@ func TestBuildUraVerifiableCredential(t *testing.T) { parser := x509_cert.NewMockChainParser(ctrl) builder := NewUraVcBuilder(creator, parser) + creator.EXPECT().CreateDid(gomock.Any(), gomock.Any()).Return("did:example:123", nil).Times(1) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { certificates, signingKey, subjectDID := tt.in() diff --git a/uzi_vc_validator/ura_validator.go b/uzi_vc_validator/ura_validator.go index 73d4287..a361e29 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/uzi_vc_validator/ura_validator.go @@ -53,12 +53,13 @@ func (u UraValidatorImpl) Validate(jwtString string) error { return err } - signingCert, err := findSigningCertificate(chainCertificates, headerValues.X509CertThumbprint) - if err != nil { - return err - } + // signingCert, err := findSigningCertificate(chainCertificates, headerValues.X509CertThumbprint) + signingCert := chainCertificates[0] + // if err != nil { + // return err + // } - err = validateChain(signingCert, chainCertificates, u.test) + err = validateChain(chainCertificates, u.test) if err != nil { return err } @@ -86,31 +87,38 @@ func (u UraValidatorImpl) Validate(jwtString string) error { return nil } -func validateChain(signingCert *x509.Certificate, certificates *[]x509.Certificate, includeTest bool) error { - var err error - intermediates := x509.NewCertPool() +// func validateChain(signingCert *x509.Certificate, certificates []*x509.Certificate, includeTest bool) error { +func validateChain(chain []*x509.Certificate, testChain bool) error { + roots := x509.NewCertPool() - for _, c := range *certificates { - if x509_cert.IsIntermediateCa(&c) { - intermediates.AddCert(&c) - } else if x509_cert.IsRootCa(&c) { - roots.AddCert(&c) + intermediates := x509.NewCertPool() + var err error + + if testChain { + roots.AddCert(chain[len(chain)-1]) + for i := 1; i < len(chain)-1; i++ { + intermediates.AddCert(chain[i]) + } + } else { + roots, intermediates, err = ca_certs.GetCertPools(testChain) + if err != nil { + return err } } - // First validate against the own provided pool - err = validate(signingCert, roots, intermediates) - if err != nil { - err = fmt.Errorf("could not validate against own provided pool: %s", err.Error()) - return err - } - root, intermediate, err := ca_certs.GetCertPools(includeTest) - if err != nil { - return err - } - err = validate(signingCert, root, intermediate) + // // First validate against the own provided pool + // err = validate(signingCert, roots, intermediates) + // if err != nil { + // err = fmt.Errorf("could not validate against own provided pool: %s", err.Error()) + // return err + // } + // root, intermediates, err := ca_certs.GetCertPools(includeTest) + // if err != nil { + // return err + // } + err = validate(chain[0], roots, intermediates) if err != nil { - err = fmt.Errorf("could not validate against the CA pool from zorgcsp (includeTest=%v): %s", includeTest, err.Error()) + err = fmt.Errorf("could not validate against the CA pool from zorgcsp (includeTest=%v): %s", testChain, err.Error()) return err } return nil @@ -128,34 +136,35 @@ func validate(signingCert *x509.Certificate, roots *x509.CertPool, intermediates return nil } -func findSigningCertificate(certificates *[]x509.Certificate, thumbprint string) (*x509.Certificate, error) { - for _, c := range *certificates { +func findSigningCertificate(certificates []*x509.Certificate, thumbprint string) (*x509.Certificate, error) { + for _, c := range certificates { hashSha1 := sha1.Sum(c.Raw) - if base64.RawURLEncoding.EncodeToString(hashSha1[:]) == thumbprint { - return &c, nil + hashedCert := base64.RawURLEncoding.EncodeToString(hashSha1[:]) + if hashedCert == thumbprint { + return c, nil } } return nil, fmt.Errorf("Could not find certificate with thumbprint %s", thumbprint) } -func parseCertificate(chain *cert.Chain) (*[]x509.Certificate, error) { - var certificates []x509.Certificate +func parseCertificate(chain *cert.Chain) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate for i := 0; i < chain.Len(); i++ { bytes, _ := chain.Get(i) blocks := pem2.ParsePemBlocks(bytes, "CERTIFICATE") - for _, block := range *blocks { + for _, block := range blocks { found, err := x509.ParseCertificates(block) if err != nil { return nil, err } for _, c := range found { if c != nil { - certificates = append(certificates, *c) + certificates = append(certificates, c) } } } } - return &certificates, nil + return certificates, nil } func parseJwtHeaderValues(jwtString string) (*JwtHeaderValues, error) { diff --git a/x509_cert/x509_cert.go b/x509_cert/x509_cert.go index d042b6d..4907410 100644 --- a/x509_cert/x509_cert.go +++ b/x509_cert/x509_cert.go @@ -43,10 +43,10 @@ func Hash(data []byte, alg string) ([]byte, error) { type ChainParser interface { // ParseCertificates parses a chain of DER-encoded certificates into an array of x509.Certificate objects. - ParseCertificates(derChain *[][]byte) (*[]x509.Certificate, error) + ParseCertificates(derChain [][]byte) ([]*x509.Certificate, error) // ParsePrivateKey parses a DER-encoded byte slice into an rsa.PrivateKey object, returning an error if parsing fails. - ParsePrivateKey(der *[]byte) (*rsa.PrivateKey, error) + ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) } // DefaultChainParser handles the parsing of certificate chains and private keys. @@ -59,33 +59,37 @@ func NewDefaultChainParser() *DefaultChainParser { // ParseCertificates parses a slice of DER-encoded byte arrays into a slice of x509.Certificate. // It returns an error if any of the certificates cannot be parsed. -func (c DefaultChainParser) ParseCertificates(derChain *[][]byte) (*[]x509.Certificate, error) { +func (c DefaultChainParser) ParseCertificates(derChain [][]byte) ([]*x509.Certificate, error) { if derChain == nil { return nil, fmt.Errorf("derChain is nil") } - chain := make([]x509.Certificate, len(*derChain)) + chain := make([]*x509.Certificate, len(derChain)) - for i, certBytes := range *derChain { + for i, certBytes := range derChain { certificate, err := x509.ParseCertificate(certBytes) if err != nil { return nil, err } - chain[i] = *certificate + chain[i] = certificate } - return &chain, nil + return chain, nil } // ParsePrivateKey parses a DER-encoded private key into an *rsa.PrivateKey. // It returns an error if the key is not in PKCS8 format or not an RSA key. -func (c DefaultChainParser) ParsePrivateKey(der *[]byte) (*rsa.PrivateKey, error) { +func (c DefaultChainParser) ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) { if der == nil { return nil, fmt.Errorf("der is nil") } - key, err := x509.ParsePKCS8PrivateKey(*der) + key, err := x509.ParsePKCS8PrivateKey(der) if err != nil { - return nil, err + key, err = x509.ParsePKCS1PrivateKey(der) + if err != nil { + return nil, err + } } + if _, ok := key.(*rsa.PrivateKey); !ok { return nil, fmt.Errorf("key is not RSA") } diff --git a/x509_cert/x509_cert_mock.go b/x509_cert/x509_cert_mock.go index 9e9fb35..0abe8f2 100644 --- a/x509_cert/x509_cert_mock.go +++ b/x509_cert/x509_cert_mock.go @@ -1,12 +1,12 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: uzi_vc_issuer/x509_cert.go +// Source: x509_cert/x509_cert.go // // Generated by this command: // -// mockgen -destination=uzi_vc_issuer/x509_cert_mock.go -package=uzi_vc_issuer -source=uzi_vc_issuer/x509_cert.go +// mockgen -destination=x509_cert/x509_cert_mock.go -package=x509_cert -source=x509_cert/x509_cert.go // -// Package uzi_vc_issuer is a generated GoMock package. +// Package x509_cert is a generated GoMock package. package x509_cert import ( @@ -41,10 +41,10 @@ func (m *MockChainParser) EXPECT() *MockChainParserMockRecorder { } // ParseCertificates mocks base method. -func (m *MockChainParser) ParseCertificates(derChain *[][]byte) (*[]x509.Certificate, error) { +func (m *MockChainParser) ParseCertificates(derChain [][]byte) ([]*x509.Certificate, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ParseCertificates", derChain) - ret0, _ := ret[0].(*[]x509.Certificate) + ret0, _ := ret[0].([]*x509.Certificate) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -56,7 +56,7 @@ func (mr *MockChainParserMockRecorder) ParseCertificates(derChain any) *gomock.C } // ParsePrivateKey mocks base method. -func (m *MockChainParser) ParsePrivateKey(der *[]byte) (*rsa.PrivateKey, error) { +func (m *MockChainParser) ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ParsePrivateKey", der) ret0, _ := ret[0].(*rsa.PrivateKey) diff --git a/x509_cert/x509_cert_test.go b/x509_cert/x509_cert_test.go index 5e94e90..f5471e7 100644 --- a/x509_cert/x509_cert_test.go +++ b/x509_cert/x509_cert_test.go @@ -93,12 +93,12 @@ func TestParseChain(t *testing.T) { testCases := []struct { name string - derChain *[][]byte + derChain [][]byte errMsg string }{ { name: "Valid Certificates", - derChain: &derChains, + derChain: derChains, }, { name: "Nil ChainPem", @@ -129,16 +129,16 @@ func TestParsePrivateKey(t *testing.T) { pkcs1PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey) testCases := []struct { name string - der *[]byte + der []byte errMsg string }{ { name: "ValidPrivateKey", - der: &privateKeyBytes, + der: privateKeyBytes, }, { name: "InvalidPrivateKey", - der: &pkcs1PrivateKey, + der: pkcs1PrivateKey, errMsg: "x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)", }, { diff --git a/x509_cert/x509_test_utils.go b/x509_cert/x509_test_utils.go index 9bf4ce9..b3b34c8 100644 --- a/x509_cert/x509_test_utils.go +++ b/x509_cert/x509_test_utils.go @@ -1,6 +1,7 @@ package x509_cert import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -8,16 +9,39 @@ import ( "encoding/asn1" "encoding/pem" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" "math/big" "strings" "time" + + "github.com/lestrrat-go/jwx/v2/cert" +) + +const ( + CertificateBlockType = "CERTIFICATE" + RSAPrivKeyBlockType = "PRIVATE KEY" ) +func EncodeRSAPrivateKey(key *rsa.PrivateKey) ([]byte, error) { + b := bytes.Buffer{} + err := pem.Encode(&b, &pem.Block{Type: RSAPrivKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}) + if err != nil { + return []byte{}, err + } + return b.Bytes(), nil +} + +func EncodeCertificates(certs ...*x509.Certificate) ([]byte, error) { + b := bytes.Buffer{} + for _, cert := range certs { + if err := pem.Encode(&b, &pem.Block{Type: CertificateBlockType, Bytes: cert.Raw}); err != nil { + return []byte{}, err + } + } + return b.Bytes(), nil +} + // BuildCertChain generates a certificate chain, including root, intermediate, and signing certificates. -func BuildCertChain(identifier string) (*[]x509.Certificate, *cert.Chain, *x509.Certificate, *rsa.PrivateKey, *x509.Certificate, error) { - chain := [4]x509.Certificate{} - chainPems := &cert.Chain{} +func BuildCertChain(identifier string) ([]*x509.Certificate, *cert.Chain, *x509.Certificate, *rsa.PrivateKey, *x509.Certificate, error) { rootKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, nil, nil, nil, err @@ -33,11 +57,6 @@ func BuildCertChain(identifier string) (*[]x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - chain[0] = *rootCert - err = chainPems.Add(rootPem) - if err != nil { - return nil, nil, nil, nil, nil, err - } intermediateL1Key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -53,11 +72,6 @@ func BuildCertChain(identifier string) (*[]x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - chain[1] = *intermediateL1Cert - err = chainPems.Add(intermediateL1Pem) - if err != nil { - return nil, nil, nil, nil, nil, err - } intermediateL2Key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -73,11 +87,6 @@ func BuildCertChain(identifier string) (*[]x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - chain[2] = *intermediateL2Cert - err = chainPems.Add(intermediateL2Pem) - if err != nil { - return nil, nil, nil, nil, nil, err - } signingKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -94,17 +103,26 @@ func BuildCertChain(identifier string) (*[]x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - chain[3] = *signingCert - err = chainPems.Add(signingPEM) - if err != nil { - return nil, nil, nil, nil, nil, err + + chain := [4]*x509.Certificate{} + for i, c := range []*x509.Certificate{signingCert, intermediateL2Cert, intermediateL1Cert, rootCert} { + chain[i] = c + } + + chainPems := &cert.Chain{} + for _, p := range [][]byte{signingPEM, intermediateL2Pem, intermediateL1Pem, rootPem} { + err = chainPems.Add(p) + if err != nil { + return nil, nil, nil, nil, nil, err + } } + chainPems, err = FixChainHeaders(chainPems) if err != nil { return nil, nil, nil, nil, nil, err } _chain := chain[:] - return &_chain, chainPems, rootCert, signingKey, signingCert, nil + return _chain, chainPems, rootCert, signingKey, signingCert, nil } // CertTemplate generates a template for a x509 certificate with a given serial number. If no serial number is provided, a random one is generated. @@ -116,6 +134,7 @@ func CertTemplate(serialNumber *big.Int) (*x509.Certificate, error) { serialNumber, _ = rand.Int(rand.Reader, serialNumberLimit) } tmpl := x509.Certificate{ + IsCA: true, SerialNumber: serialNumber, Subject: pkix.Name{Organization: []string{"JaegerTracing"}}, SignatureAlgorithm: x509.SHA256WithRSA, diff --git a/x509_cert/x509_utils.go b/x509_cert/x509_utils.go index 78292a5..81a2e07 100644 --- a/x509_cert/x509_utils.go +++ b/x509_cert/x509_utils.go @@ -30,13 +30,14 @@ func FindOtherName(certificate *x509.Certificate) (string, SanTypeName, error) { if otherNameValue != "" { return otherNameValue, SAN_TYPE_OTHER_NAME, nil } - err = errors.New("no certificate found in the SAN attributes, please check if the certificate is an UZI Server Certificate") + err = errors.New("no otherName found in the SAN attributes, please check if the certificate is an UZI Server Certificate") return "", "", err } func findOtherNameValue(cert *x509.Certificate) (string, error) { value := "" for _, extension := range cert.Extensions { + fmt.Println("extension.Id: ", extension.Id) if extension.Id.Equal(SubjectAlternativeNameType) { err := forEachSAN(extension.Value, func(tag int, data []byte) error { if tag != 0 { @@ -47,6 +48,7 @@ func findOtherNameValue(cert *x509.Certificate) (string, error) { if err != nil { return fmt.Errorf("could not parse requested other SAN: %v", err) } + fmt.Println("other.TypeID: ", other.TypeID, OtherNameType) if other.TypeID.Equal(OtherNameType) { _, err = asn1.Unmarshal(other.Value.Bytes, &value) if err != nil { @@ -118,19 +120,21 @@ func IsIntermediateCa(signingCert *x509.Certificate) bool { // FindSigningCertificate searches the provided certificate chain for a certificate with a specific SAN and Permanent Identifier. // It returns the found certificate, its IdentifierValue, and an error if no matching certificate is found. -func FindSigningCertificate(chain *[]x509.Certificate) (*x509.Certificate, string, error) { - if len(*chain) == 0 { +func FindSigningCertificate(chain []*x509.Certificate) (*x509.Certificate, string, error) { + if len(chain) == 0 { return nil, "", fmt.Errorf("no certificates provided") } var err error var otherNameValue string - for _, c := range *chain { - otherNameValue, _, err = FindOtherName(&c) + for _, c := range chain { + otherNameValue, _, err = FindOtherName(c) if err != nil { + fmt.Printf("info: no SAN in certificate: %v\n", err) continue } if otherNameValue != "" { - return &c, otherNameValue, nil + fmt.Printf("info: found SAN in certificate: %v\n", otherNameValue) + return c, otherNameValue, nil } } return nil, "", err From 109fad46f0ef72dcfd9990d31f8e44f284a519aa Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Mon, 14 Oct 2024 17:00:04 +0200 Subject: [PATCH 02/13] Prevented infinite loop - Multiple CA roots co-exist when the -t flag is enabled. --- uzi_vc_issuer/ura_issuer.go | 40 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go index 64f508a..7c7923a 100644 --- a/uzi_vc_issuer/ura_issuer.go +++ b/uzi_vc_issuer/ura_issuer.go @@ -216,36 +216,24 @@ func BuildCertificateChain(certs []*x509.Certificate) []*x509.Certificate { var chain []*x509.Certificate chain = append(chain, signingCert) - var root *x509.Certificate - if !x509_cert.IsRootCa(signingCert) { - // find the root certificate + certToCheck := signingCert + for !x509_cert.IsRootCa(certToCheck) { + found := false for _, c := range certs { - if x509_cert.IsRootCa(c) { - root = c + if c.Equal(signingCert) { + continue + } + err := certToCheck.CheckSignatureFrom(c) + if err == nil { + chain = append(chain, c) + certToCheck = c + found = true break } } - - certToCheck := signingCert - for !certToCheck.Equal(root) { - found := false - for _, c := range certs { - if c.Equal(signingCert) { - continue - } - err := certToCheck.CheckSignatureFrom(c) - if err == nil { - - chain = append(chain, c) - certToCheck = c - found = true - break - } - } - if !found { - fmt.Println("failed to find path from signingCert to root") - return nil - } + if !found { + fmt.Println("failed to find path from signingCert to root") + return nil } } return chain From 7e88b996ecfb2d06a505c5215e6e41ffa0ea17ae Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:08:36 +0200 Subject: [PATCH 03/13] 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. --- x509_cert/x509_utils.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/x509_cert/x509_utils.go b/x509_cert/x509_utils.go index 81a2e07..4bf3674 100644 --- a/x509_cert/x509_utils.go +++ b/x509_cert/x509_utils.go @@ -37,7 +37,6 @@ func FindOtherName(certificate *x509.Certificate) (string, SanTypeName, error) { func findOtherNameValue(cert *x509.Certificate) (string, error) { value := "" for _, extension := range cert.Extensions { - fmt.Println("extension.Id: ", extension.Id) if extension.Id.Equal(SubjectAlternativeNameType) { err := forEachSAN(extension.Value, func(tag int, data []byte) error { if tag != 0 { @@ -48,7 +47,6 @@ func findOtherNameValue(cert *x509.Certificate) (string, error) { if err != nil { return fmt.Errorf("could not parse requested other SAN: %v", err) } - fmt.Println("other.TypeID: ", other.TypeID, OtherNameType) if other.TypeID.Equal(OtherNameType) { _, err = asn1.Unmarshal(other.Value.Bytes, &value) if err != nil { From 2eaf1d21c24bfdeb943141071397a869cba03781 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:11:30 +0200 Subject: [PATCH 04/13] 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. --- uzi_vc_issuer/ura_issuer.go | 19 +++++++---------- uzi_vc_validator/ura_validator.go | 34 +++++++++++-------------------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go index 7c7923a..ba0bba3 100644 --- a/uzi_vc_issuer/ura_issuer.go +++ b/uzi_vc_issuer/ura_issuer.go @@ -33,24 +33,19 @@ import ( var RegexOtherNameValue = regexp.MustCompile(`\d+-\d+-S-(\d+)-00\.000-\d+`) // Issue generates a URA Verifiable Credential using provided certificate, signing key, subject DID, and subject name. -func Issue(certificateFile string, signingKeyFile string, subjectDID string, test bool) (string, error) { +func Issue(certificateFile string, signingKeyFile string, subjectDID string, allowTestUraCa bool) (string, error) { pemBlocks, err := pem2.ParseFileOrPath(certificateFile, "CERTIFICATE") if err != nil { return "", err } - + allowSelfSignedCa := len(pemBlocks) > 1 if len(pemBlocks) == 1 { - if !test { - err = fmt.Errorf("did not find exactly one certificate in file %s", certificateFile) + certificate := pemBlocks[0] + pemBlocks, err = ca_certs.GetDERs(allowTestUraCa) + if err != nil { return "", err - } else { - certificate := pemBlocks[0] - pemBlocks, err = ca_certs.GetDERs(test) - if err != nil { - return "", err - } - pemBlocks = append(pemBlocks, certificate) } + pemBlocks = append(pemBlocks, certificate) } signingKeys, err := pem2.ParseFileOrPath(signingKeyFile, "PRIVATE KEY") @@ -86,7 +81,7 @@ func Issue(certificateFile string, signingKeyFile string, subjectDID string, tes if err != nil { return "", err } - validator := uzi_vc_validator.NewUraValidator(test) + validator := uzi_vc_validator.NewUraValidator(allowTestUraCa, allowSelfSignedCa) jwtString := string(credentialJSON) jwtString = jwtString[1:] // Chop start jwtString = jwtString[:len(jwtString)-1] // Chop end diff --git a/uzi_vc_validator/ura_validator.go b/uzi_vc_validator/ura_validator.go index dc3b9ea..6889b06 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/uzi_vc_validator/ura_validator.go @@ -22,7 +22,12 @@ type UraValidator interface { } type UraValidatorImpl struct { - test bool + allowUziTestCa bool + allowSelfSignedCa bool +} + +func NewUraValidator(allowUziTestCa bool, allowSelfSignedCa bool) *UraValidatorImpl { + return &UraValidatorImpl{allowUziTestCa, allowSelfSignedCa} } type JwtHeaderValues struct { @@ -58,7 +63,7 @@ func (u UraValidatorImpl) Validate(jwtString string) error { // return err // } - err = validateChain(chainCertificates, u.test) + err = validateChain(signingCert, chainCertificates, u.allowUziTestCa, u.allowSelfSignedCa) if err != nil { return err } @@ -87,37 +92,26 @@ func (u UraValidatorImpl) Validate(jwtString string) error { } // func validateChain(signingCert *x509.Certificate, certificates []*x509.Certificate, includeTest bool) error { -func validateChain(chain []*x509.Certificate, testChain bool) error { +func validateChain(signingCert *x509.Certificate, chain []*x509.Certificate, allowUziTestCa bool, allowSelfSignedCa bool) error { roots := x509.NewCertPool() intermediates := x509.NewCertPool() var err error - if testChain { + if allowSelfSignedCa { roots.AddCert(chain[len(chain)-1]) for i := 1; i < len(chain)-1; i++ { intermediates.AddCert(chain[i]) } } else { - roots, intermediates, err = ca_certs.GetCertPools(testChain) + roots, intermediates, err = ca_certs.GetCertPools(allowUziTestCa) if err != nil { return err } } - - // // First validate against the own provided pool - // err = validate(signingCert, roots, intermediates) - // if err != nil { - // err = fmt.Errorf("could not validate against own provided pool: %s", err.Error()) - // return err - // } - // root, intermediates, err := ca_certs.GetCertPools(includeTest) - // if err != nil { - // return err - // } - err = validate(chain[0], roots, intermediates) + err = validate(signingCert, roots, intermediates) if err != nil { - err = fmt.Errorf("could not validate against the CA pool from zorgcsp (includeTest=%v): %s", testChain, err.Error()) + err = fmt.Errorf("could not validate against the CA pool. %s", err.Error()) return err } return nil @@ -181,7 +175,3 @@ func parseJwtHeaderValues(jwtString string) (*JwtHeaderValues, error) { } return metadata, nil } - -func NewUraValidator(test bool) *UraValidatorImpl { - return &UraValidatorImpl{test} -} From 622cef41c418c0984992ac3219ccaa147a7b1e06 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:12:05 +0200 Subject: [PATCH 05/13] 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. --- uzi_vc_validator/ura_validator.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/uzi_vc_validator/ura_validator.go b/uzi_vc_validator/ura_validator.go index 6889b06..9f249cb 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/uzi_vc_validator/ura_validator.go @@ -57,11 +57,10 @@ func (u UraValidatorImpl) Validate(jwtString string) error { return err } - // signingCert, err := findSigningCertificate(chainCertificates, headerValues.X509CertThumbprint) - signingCert := chainCertificates[0] - // if err != nil { - // return err - // } + signingCert, err := findSigningCertificate(chainCertificates, headerValues.X509CertThumbprint) + if err != nil { + return err + } err = validateChain(signingCert, chainCertificates, u.allowUziTestCa, u.allowSelfSignedCa) if err != nil { From b093d0f68c76f3efd156f2a9ad920a9f98549066 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:12:19 +0200 Subject: [PATCH 06/13] 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. --- main.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 5a1be53..ae49d22 100644 --- a/main.go +++ b/main.go @@ -9,14 +9,16 @@ import ( ) 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 { - Identifier string `arg:"" name:"identifier" help:"Identifier for the test certificate such as an URA or UZI number."` + 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."` } var CLI struct { @@ -45,10 +47,13 @@ func main() { os.Exit(-1) } println(jwt) - case "test-cert ": - otherName := fmt.Sprintf("2.16.528.1.1007.1.%s", cli.TestCert.Identifier) + case "test-cert ": + // Format is 2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344 + // ------ + // 2.16.528.1.1007.99.2110-1--S--00.000- + 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(cli.TestCert.Identifier) + chain, _, _, privKey, _, err := x509_cert.BuildCertChain(otherName) if err != nil { fmt.Println(err) os.Exit(-1) From 976057c49d9fed5d0fc90a93f031b85a665161b4 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:26:10 +0200 Subject: [PATCH 07/13] 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. --- main.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index ae49d22..1d8f54b 100644 --- a/main.go +++ b/main.go @@ -70,13 +70,17 @@ func main() { os.Exit(-1) } - os.WriteFile("chain.pem", chainPems, 0644) - os.WriteFile("signing_key.pem", signingKeyPem, 0644) - + 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) } + default: fmt.Println("Unknown command") os.Exit(-1) From 24ad91608c275648962af653d4b277ca72d507d6 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:26:18 +0200 Subject: [PATCH 08/13] 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. --- did_x509/did_x509.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/did_x509/did_x509.go b/did_x509/did_x509.go index 91687e3..1738740 100644 --- a/did_x509/did_x509.go +++ b/did_x509/did_x509.go @@ -21,9 +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(ca *x509.Certificate, policy string) (string, error) { +func FormatDid(caCert *x509.Certificate, policy string) (string, error) { alg := "sha512" - rootHash, err := x509_cert.Hash(ca.Raw, alg) + rootHash, err := x509_cert.Hash(caCert.Raw, alg) if err != nil { return "", err } @@ -77,7 +77,7 @@ 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 { + if x509_cert.IsRootCa(cert) { return cert, nil } } From b09d7cc7418fef425b393b20f9bd683b6147db44 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:59:34 +0200 Subject: [PATCH 09/13] 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. --- x509_cert/x509_test_utils.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x509_cert/x509_test_utils.go b/x509_cert/x509_test_utils.go index b8122a0..239b235 100644 --- a/x509_cert/x509_test_utils.go +++ b/x509_cert/x509_test_utils.go @@ -45,7 +45,7 @@ func BuildCertChain(identifier string) ([]*x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - rootCertTmpl, err := CertTemplate(nil) + rootCertTmpl, err := CertTemplate(nil, "Root CA") if err != nil { return nil, nil, nil, nil, nil, err } @@ -61,7 +61,7 @@ func BuildCertChain(identifier string) ([]*x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL1Tmpl, err := CertTemplate(nil) + intermediateL1Tmpl, err := CertTemplate(nil, "Intermediate CA Level 1") if err != nil { return nil, nil, nil, nil, nil, err } @@ -76,7 +76,7 @@ func BuildCertChain(identifier string) ([]*x509.Certificate, *cert.Chain, *x509. if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL2Tmpl, err := CertTemplate(nil) + intermediateL2Tmpl, err := CertTemplate(nil, "Intermediate CA Level 2") if err != nil { return nil, nil, nil, nil, nil, err } @@ -126,7 +126,7 @@ func BuildCertChain(identifier string) ([]*x509.Certificate, *cert.Chain, *x509. // CertTemplate generates a template for a x509 certificate with a given serial number. If no serial number is provided, a random one is generated. // The certificate is valid for one month and uses SHA256 with RSA for the signature algorithm. -func CertTemplate(serialNumber *big.Int) (*x509.Certificate, error) { +func CertTemplate(serialNumber *big.Int, organization string) (*x509.Certificate, error) { // generate a random serial number (a real cert authority would have some logic behind this) if serialNumber == nil { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) @@ -135,7 +135,7 @@ func CertTemplate(serialNumber *big.Int) (*x509.Certificate, error) { tmpl := x509.Certificate{ IsCA: true, SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{"JaegerTracing"}}, + Subject: pkix.Name{Organization: []string{organization}}, SignatureAlgorithm: x509.SHA256WithRSA, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month @@ -180,11 +180,11 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string) (*x509.Certif tmpl := x509.Certificate{ SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{"JaegerTracing"}}, + Subject: pkix.Name{Organization: []string{"FauxCare"}}, SignatureAlgorithm: x509.SHA256WithRSA, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 30), // valid for a month - EmailAddresses: []string{"roland@edia.nl"}, + EmailAddresses: []string{"roland@headease.nl"}, BasicConstraintsValid: true, ExtraExtensions: []pkix.Extension{ { From b52af6f9f72bb11ad5f91680b6b99b3d76ccd367 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 00:59:48 +0200 Subject: [PATCH 10/13] 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. --- main.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 1d8f54b..fde4757 100644 --- a/main.go +++ b/main.go @@ -16,9 +16,10 @@ type VC struct { } 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."` + 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 { @@ -47,7 +48,7 @@ func main() { os.Exit(-1) } println(jwt) - case "test-cert ": + case "test-cert ", "test-cert ": // Format is 2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344 // ------ // 2.16.528.1.1007.99.2110-1--S--00.000- @@ -80,7 +81,18 @@ func main() { 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) From 8e67b8c7f0ab7b7592e1e603b077924e35c9ff09 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 13:10:12 +0200 Subject: [PATCH 11/13] Potentially unsafe quoting: Refactor JSON unmarshal logic in Validate method Replaced the string formatting approach with direct JSON marshaling to prevent potentially unsafe quoting. --- uzi_vc_validator/ura_validator.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/uzi_vc_validator/ura_validator.go b/uzi_vc_validator/ura_validator.go index 294cfb8..5951e3f 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/uzi_vc_validator/ura_validator.go @@ -39,7 +39,8 @@ type JwtHeaderValues struct { func (u UraValidatorImpl) Validate(jwtString string) error { credential := &vc.VerifiableCredential{} - err := json.Unmarshal([]byte(fmt.Sprintf("\"%s\"", jwtString)), credential) + marshal, _ := json.Marshal(jwtString) + err := json.Unmarshal(marshal, credential) if err != nil { return err } @@ -81,7 +82,7 @@ func (u UraValidatorImpl) Validate(jwtString string) error { } if ura != parseDid.Ura { - return fmt.Errorf("Ura in credential does not match Ura in signing certificate") + return fmt.Errorf("URA in credential does not match Ura in signing certificate") } if sanType != parseDid.SanType { return fmt.Errorf("SanType in credential does not match SanType in signing certificate") From 83c698926d880a4c695a9acab89e994f5e5ece57 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 13:11:04 +0200 Subject: [PATCH 12/13] Refactor variable name in EncodeCertificates function Renamed the loop variable from 'cert' to 'c' to prevent a naming collision with the cert import. --- x509_cert/x509_test_utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x509_cert/x509_test_utils.go b/x509_cert/x509_test_utils.go index 239b235..ea2861c 100644 --- a/x509_cert/x509_test_utils.go +++ b/x509_cert/x509_test_utils.go @@ -31,8 +31,8 @@ func EncodeRSAPrivateKey(key *rsa.PrivateKey) ([]byte, error) { func EncodeCertificates(certs ...*x509.Certificate) ([]byte, error) { b := bytes.Buffer{} - for _, cert := range certs { - if err := pem.Encode(&b, &pem.Block{Type: CertificateBlockType, Bytes: cert.Raw}); err != nil { + for _, c := range certs { + if err := pem.Encode(&b, &pem.Block{Type: CertificateBlockType, Bytes: c.Raw}); err != nil { return []byte{}, err } } From 9de9fc9ea227fecb6ae7029c96814918d2c89057 Mon Sep 17 00:00:00 2001 From: Roland Groen Date: Tue, 15 Oct 2024 13:55:33 +0200 Subject: [PATCH 13/13] 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. --- x509_cert/x509_test_utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/x509_cert/x509_test_utils.go b/x509_cert/x509_test_utils.go index ea2861c..d2f825f 100644 --- a/x509_cert/x509_test_utils.go +++ b/x509_cert/x509_test_utils.go @@ -231,6 +231,7 @@ func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv } // DebugUnmarshall recursively unmarshalls ASN.1 encoded data and prints the structure with parsed values. +// Keep this method for debug purposes in the future. func DebugUnmarshall(data []byte, depth int) error { for len(data) > 0 { var x asn1.RawValue