diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bb04ed4..a680572 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -35,4 +35,4 @@ jobs: with: coverageLocations: | ${{github.workspace}}/c.out:gocov - prefix: github.com/nuts-foundation/uzi-did-x509-issuer + prefix: github.com/nuts-foundation/go-didx509-toolkit diff --git a/.gitignore b/.gitignore index 16a4a6b..d76289a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /*.pem !ca.pem -uzi-did-x509-issuer +go-didx509-toolkit c.out .idea ./issuer diff --git a/NUTS_README.md b/NUTS_README.md deleted file mode 100644 index a89a7a4..0000000 --- a/NUTS_README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Using the Nuts *UZI Server Certificaat Issuer* with *NUTS* -This guide describes how to load the generated VCs by the *UZI Server Certificaat Issuer* into a NUTS node. - -## Prerequisites -The following is required to load the VC into the NUTS node - * A UZI Server Certificate and private key, either a test or production. In order to use the test certificate, the `-t` option must be provided. - * This tool, make sure to run `make build` for generating the binary. For more details see the [README.md](README.md) file. - * A running NUTS node to create a did and load the certificate. The NUTS node will be referred to as ``${nuts_base_url}`` - * The NUTS node should have the [CA](http://cert.pkioverheid.nl/PrivateRootCA-G1.cer) cert added to the ca certificate chain `tls.truststorefile`. - * The same goes for the test certificate, the [Test CA](http://www.uzi-register-test.nl/cacerts/test_zorg_csp_private_root_ca_g1.cer) should be added to the ca certificate chain `tls.truststorefile`. - -## Issuing a VC to a NUTS node. -### Creating the subject (only once) -#### request -Parameters: - * `nuts_base_url`: the internal base URL of the NUTS node - * `subject`: the NUTS subject used to issue the VC to -```shell -curl --location '${nuts_base_url}/internal/vdr/v2/subject' \ ---header 'Content-Type: application/json' \ ---header 'Accept: application/json' \ ---data '{ - "subject": "${subject}" -}' -``` -#### response -```json -{ - "documents": [ - { - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" - ], - ... - } - ], - "subject": "zbj_test" -} -``` -### Fetch the did -#### request -* `nuts_base_url`: the internal base URL of the NUTS node -* `subject`: the NUTS subject used to issue the VC to -```shell -curl --location '${nuts_base_url}/internal/vdr/v2/subject/${subject}' \ ---header 'Accept: application/json' -``` -#### response -The response contains the did that will be used for issuance. -```json -[ - "did:web:example.com:iam:809c0e66-dba7-496b-96b9-cd3ac71c34ff" -] -``` -### Generate the VC -* `certificate_file`: the certificate file -* `key_file`: the private key paired with the certificate file -* `did`: the NUTS subject did -```shell -./issuer vc "${certificate_file}" "${key_file}" "${did}" -``` -Note, for test certificates, use the `-t` flag. -```shell -./issuer vc "${certificate_file}" "${key_file}" "${did}" -t -``` -The output looks like (abbreviated): -```text -ey... -``` -### Load the VC into the NUTS node -* `nuts_base_url`: the internal base URL of the NUTS node -* `subject`: the NUTS subject used to issue the VC to -* `vc`: The VC from the previous step, note that the body is JSON and the VC should be surrounded with double quotes ("). -```shell -curl --location '${nuts_base_url}/internal/vcr/v2/holder/${subject}/vc' \ ---header 'Content-Type: application/json' \ ---data '"${vc}"' -``` - -### Verify the VC's presence in NUTS -#### request -* `nuts_base_url`: the internal base URL of the NUTS node -* `subject`: the NUTS subject used to issue the VC to -```shell -curl --location '${nuts_base_url}/internal/vcr/v2/holder/${subject}/vc' \ ---header 'Accept: application/json' -``` -#### response -```json -[ - "ey..." -] -``` diff --git a/README.md b/README.md index 6365df2..c0b8e33 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,48 @@ -# Nuts X509 Certificate Issuer +# Golang did:x509 and X509Credential Toolkit -[![Maintainability](https://api.codeclimate.com/v1/badges/f92496250890e40900aa/maintainability)](https://codeclimate.com/github/nuts-foundation/uzi-did-x509-issuer/maintainability) -[![Test Coverage](https://api.codeclimate.com/v1/badges/f92496250890e40900aa/test_coverage)](https://codeclimate.com/github/nuts-foundation/uzi-did-x509-issuer/test_coverage) - -> [!CAUTION] -> This repository contains experimental code and is not suitable for production usage! +[![Maintainability](https://api.codeclimate.com/v1/badges/f92496250890e40900aa/maintainability)](https://codeclimate.com/github/nuts-foundation/go-didx509-toolkit/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/f92496250890e40900aa/test_coverage)](https://codeclimate.com/github/nuts-foundation/go-didx509-toolkit/test_coverage) ## Description -The X509 certificate Issuer is a Go-based tool designed for issuing Verifiable Credentials signed by a X509 certificate. The issuer creates a did:x509 based on the PKI certificate chain. -Its main purspose is to create verificable credentials form certificates issued by the [UZI certificate chain from the CIBG registry](https://www.zorgcsp.nl/ca-certificaten). - -## Features - -The X509 certificate Issuer generated a Verifiable Credential of type X509Credential with the following features: - -- The DID method is a customized did:x509 DID pointing to the x5c header. -- The x5c filled with the certificate chain. The chain is built from: - - The provided UZI server (Test) certificate - - All the required certificates from the [UZI register](https://www.zorgcsp.nl/certificate-revocation-lists-crl-s). - - If the test mode is enabled, the [Test UZI register](https://acceptatie.zorgcsp.nl/ca-certificaten) -- Signed by the private key of the X509 certificate. -- The VC issued to the provided DID and name. - -## Note on security, trust, and secrecy - -The VC that is signed by this application are cryptographic proofs, signed by the private key used in the X509 certificate process. Note that: - -- This private key is supposed to be kept very secret. -- The Subject DID of the signed credential is mandated with cryptographic proof to act on behalf of the owner of the private key on the NUTS network. - -## Prerequisites - -Before you begin, ensure you have met the following requirements: - -- You have installed Go SDK 1.23.1 or compatible version. -- You are using a Unix-based operating system like macOS or Linux. -- You have the necessary permissions to install software and manage certificates. - -## Installation +This is a Golang-based toolkit for creating `did:x509` DIDs and `X509Credential`s. +`X509Credential`s can be used present the identity information contained in the `did:x509` DID as Verifiable Credential. -Follow these steps to set up the project: +Its original purpose is to create Verifiable Credentials from certificates issued by the [UZI certificate chain from the CIBG registry](https://www.zorgcsp.nl/ca-certificaten). -1. **Clone the repository:** - ```sh - git clone https://github.com/nuts-foundation/uzi-did-x509-issuer - ``` -2. **Change to the project directory:** - ```sh - cd uzi-did-x509-issuer - ``` -3. **Download dependencies:** - ```sh - go mod download && go mod verify - ``` -4. **Build the project:** - ```sh - go build -ldflags="-w -s " -o ./issuer - ``` - or - ```shell - make build - ``` - -## Usage - -1. **Run the application:** - - ```sh - ./issuer - ``` - -2. **Getting command line help:** - - Use the CLI options provided by the application to generate new certificates. Refer to the help command for more details: - ```sh - ./issuer --help - ``` -3. **Call for generating a VC:** - - The following parameters are required: - - **certificate_file**, the PEM file of the URA server certificate - - **signing_key** ,the unencrypted PEM file of the private key used for signing. - - **subject_did** and **subject_name**, the vc.subject.id and vc.subject.name of the generated verifiable credential. - -### Examples +## Features -- **Example call with a TEST certificate** - ``` - ./issuer vc cert.pem key.key did:web:example.com:example --test - ``` -- **Example call with a production certificate** - ``` - ./issuer vc cert.pem key.key did:web:example.com:example - ``` +### Creating `did:x509` DIDs -## Project UZI CA and Intermediate CA files +The toolkit creates `did:x509` DIDs as specified by https://trustoverip.github.io/tswg-did-x509-method-specification/. +It extends this DID method specification by adding support for the `san:otherName` field in the certificate (required by the CIBG UZI certificate use case). -This project downloads the relevant CA certs from: +### Issuing `X509Credential`s -- [https://www.zorgcsp.nl/ca-certificaten](https://www.zorgcsp.nl/ca-certificaten) -- [https://acceptatie.zorgcsp.nl/ca-certificaten](https://acceptatie.zorgcsp.nl/ca-certificaten) +The primary use of this toolkit is self-issuing `X509Credential`s through a `did:x509` DID, backed by an X.509 certificate. +To issue an `X509Credential`, provide the following parameters: -## Converting to PEM files: +- **certificate_file**: the PEM file of the certificate +- **signing_key**: the unencrypted PEM file of the private key used for signing. +- **credential_subject**: the ID of the credential subject, typically a DID. -The following command converts .cer files to PEM: +Usage: +```shell +./issuer vc +``` +Example: ```shell - openssl x509 -inform der -in certificate.cer -out certificate.pem +./issuer vc certificate.pem key.pem did:web:example.com ``` -## Validating a X509Credential +### Validating `X509Credential`s + +TODO -The logic on Validating a X509Credential is described in the [VC_VALIDATION.md](VC_VALIDATION.md) file. +## Limitations + +Only RSA keys are supported at the moment. ## Contributing @@ -127,8 +59,4 @@ Please ensure your code follows the project's coding conventions and passes all ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - -## Contact - -If you have any questions or suggestions, feel free to open an issue or contact the project maintainers at [roland@headease.nl](mailto:roland@headease.nl). +This project is licensed under the GPLv3 License. See the [LICENSE](LICENSE) file for details. diff --git a/VC_VALIDATION.md b/VC_VALIDATION.md deleted file mode 100644 index b033fbf..0000000 --- a/VC_VALIDATION.md +++ /dev/null @@ -1,64 +0,0 @@ -# Validating a X509Credential - -This specification explains how to validate a Verifiable Credential of this type. - -## About the UZI Server Certificate - -UZI Server Certificates contain the URA number in the `san:otherName` field encoded in a compound string: - -``` ------- -``` - -After 8 nov 2023 the UZI Server Certificates also has the URA number in the `san:otherName.permanentIdentifier` field. - -## Structure of the Verifiable Credential - -The Verifiable Credential has the following structure: - -1. The credential has a type `X509Credential`. -2. The `subject.id` points to the holder of the credential, typically a `did:nuts` or `did:web`. -3. The credential is issued by a `did:x509`, with changes defined in the - section [Changes to the did:x509 Method Specification](#changes-to-the-didx509-method-specification), as part of - this specification: - 1. The `x5c` header contains the UZI Server Certificate with the full certificate chain. - 2. The `x5t` header contains the sha1 hash of the UZI Server Certificate. - 3. The policy string of the `did:x509` contains either a `san:otherName.permanentIdentifier:` or - `san:otherName:` policy. - 4. If the `san:otherName:` is present, the URA number should be found as part of the `san:otherName` - field. - 5. If the `san:otherName.permanentIdentifier:` is present, the URA number should be found as part of the - `san:otherName.permanentIdentifier` field. - -## Validating a X509Credential Verifiable Credential - -A X509Credential is valid when: - -1. The credential MUST be of type `X509Credential`. -2. The `x5c` header MUST contain the UZI Server Certificate with the full certificate chain. -3. The `x5t` header MUST contain the sha1 hash of the UZI Server Certificate. -4. The signature of the Verifiable Credential MUST validate against the public key of the UZI Server Certificate. -5. The UZI Server Certificate chain MUST be valid and match - the [UZI-register certificate chain](https://www.zorgcsp.nl/ca-certificaten). -6. The issuer of the credential MUST be a `did:x509` with changes defined in the - section [Changes to the did:x509 Method Specification](#changes-to-the-didx509-method-specification). -7. The issuer of the credential MUST have an `san:otherName:` policy. -8. The value of `` MUST match the value of the - `SubjectAltName (2.5.29.17)` `OtherName (2.5.5.5)` with the group 1 of the following regular expression as the URA number: - ```regexp - 2\.16\.528\.1\.1007.\d+\.\d+-\d+-\d+-S-(\d+)-00\.000-\d+ - ``` - -## Changes to the did:x509 Method Specification - -The X509Credential makes use of an additional otherName san-type. This -san-type is currently not part of the x509 standard. The suggested policy definition will look like this: - -``` -policy-name = "san" -policy-value = san-type ":" san-value -san-type = "email" / "dns" / "uri" / "otherName" -san-value = 1*idchar -``` - -A request to support this will be diff --git a/ca_certs/uzi_ca_certs.go b/ca_certs/uzi_ca_certs.go deleted file mode 100644 index 7e6a913..0000000 --- a/ca_certs/uzi_ca_certs.go +++ /dev/null @@ -1,142 +0,0 @@ -package ca_certs - -import ( - "bytes" - "crypto/x509" - "fmt" - "io" - "net/http" -) - -type UziCaPool struct { - rootCaUrls []string - intermediateCaUrls []string -} - -var ( - ProductionUziCaPool = UziCaPool{ - rootCaUrls: []string{"http://cert.pkioverheid.nl/PrivateRootCA-G1.cer"}, - intermediateCaUrls: []string{"http://cert.pkioverheid.nl/DomPrivateServicesCA-G1.cer", "http://cert.pkioverheid.nl/UZI-register_Private_Server_CA_G1.cer"}, - } - TestUziCaPool = UziCaPool{ - rootCaUrls: []string{"http://www.uzi-register-test.nl/cacerts/test_zorg_csp_private_root_ca_g1.cer"}, - intermediateCaUrls: []string{"http://www.uzi-register-test.nl/cacerts/test_zorg_csp_level_2_private_services_ca_g1.cer", "http://www.uzi-register-test.nl/cacerts/test_uzi-register_private_server_ca_g1.cer"}, - } -) - -func GetCertPools(includeTest bool) (root *x509.CertPool, intermediate *x509.CertPool, err error) { - pool := prepareAndCombinePools(includeTest) - return downloadUziPool(pool) -} - -func GetCerts(includeTest bool) ([]*x509.Certificate, error) { - pool := prepareAndCombinePools(includeTest) - return downloadUziPoolCerts(pool) -} - -func GetDERs(includeTest bool) ([][]byte, error) { - pool := prepareAndCombinePools(includeTest) - return downloadUziPoolDERs(pool) -} - -func prepareAndCombinePools(includeTest bool) UziCaPool { - pool := UziCaPool{} - pool.rootCaUrls = ProductionUziCaPool.rootCaUrls - pool.intermediateCaUrls = ProductionUziCaPool.intermediateCaUrls - if includeTest { - pool.rootCaUrls = append(pool.rootCaUrls, TestUziCaPool.rootCaUrls...) - pool.intermediateCaUrls = append(pool.intermediateCaUrls, TestUziCaPool.intermediateCaUrls...) - } - return pool -} - -func downloadUziPoolDERs(pool UziCaPool) ([][]byte, error) { - var rv = [][]byte{} - certs, err := downloadUziPoolCerts(pool) - if err != nil { - return nil, err - } - for _, cert := range certs { - rv = append(rv, cert.Raw) - } - return rv, err -} - -func GetTestCerts() ([]*x509.Certificate, error) { - return downloadUziPoolCerts(TestUziCaPool) -} - -// Internal Helper Functions - -func downloadUziPool(pool UziCaPool) (*x509.CertPool, *x509.CertPool, error) { - roots, err := downloadPool(pool.rootCaUrls) - if err != nil { - return nil, nil, err - } - intermediates, err := downloadPool(pool.intermediateCaUrls) - if err != nil { - return nil, nil, err - } - - return roots, intermediates, nil -} - -func downloadUziPoolCerts(pool UziCaPool) ([]*x509.Certificate, error) { - allUrls := append(pool.rootCaUrls, pool.intermediateCaUrls...) - all, err := downloadCerts(allUrls) - if err != nil { - return nil, err - } - - return all, nil -} - -func downloadPool(urls []string) (*x509.CertPool, error) { - roots := x509.NewCertPool() - for _, url := range urls { - certificate, err := readCertificateFromUrl(url) - if err != nil { - return nil, err - } - roots.AddCert(certificate) - } - return roots, nil -} - -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) - } - return certs, nil -} - -func readCertificateFromUrl(url string) (*x509.Certificate, error) { - response, err := http.Get(url) - if err != nil { - return nil, err - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - fmt.Printf("Error closing body: %v", err) - } - }(response.Body) - - if response.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code (%v) from url %s: ", response.StatusCode, url) - } - - buffer := bytes.Buffer{} - _, err = io.Copy(&buffer, response.Body) - if err != nil { - return nil, err - } - - certificate, err := x509.ParseCertificate(buffer.Bytes()) - return certificate, err -} diff --git a/ca_certs/uzi_ca_certs_test.go b/ca_certs/uzi_ca_certs_test.go deleted file mode 100644 index 23adc27..0000000 --- a/ca_certs/uzi_ca_certs_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package ca_certs - -import ( - "testing" -) - -func TestGetCertPools(t *testing.T) { - // Define the test cases - tests := []struct { - name string - includeTest bool - expectedRootLen int - expectedIntermediateLen int - expectedError error - }{ - { - name: "Test case 1: With Test Certificate included", - includeTest: true, - expectedRootLen: 2, - expectedIntermediateLen: 4, - expectedError: nil, - }, - { - name: "Test case 2: Without Test Certificate", - includeTest: false, - expectedRootLen: 1, - expectedIntermediateLen: 2, - expectedError: nil, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Call the function we want to test - root, intermediate, err := GetCertPools(tc.includeTest) - - if tc.expectedError != nil { - // If we were expecting an error and we got one, then continue to next test case - if err != nil { - return - } - - // If we were expecting an error and didn't get one, then report it - t.Fatalf("expected error but got nil") - } - - // Make sure we got what we expected - if len(root.Subjects()) != tc.expectedRootLen || len(intermediate.Subjects()) != tc.expectedIntermediateLen { - t.Errorf("expected root or intermediate certificate pools but got nil") - } - - }) - } -} diff --git a/credential_issuer/issuer.go b/credential_issuer/issuer.go new file mode 100644 index 0000000..d3edb4f --- /dev/null +++ b/credential_issuer/issuer.go @@ -0,0 +1,194 @@ +package credential_issuer + +import ( + "context" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "errors" + "github.com/nuts-foundation/go-didx509-toolkit/internal" + "time" + + "github.com/nuts-foundation/go-did/did" + + "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/go-didx509-toolkit/did_x509" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" +) + +// CredentialType holds the name of the X.509 credential type. +var CredentialType = ssi.MustParseURI("X509Credential") + +// issueOptions contains values for options for issuing a UZI VC. +type issueOptions struct { + includePermanentIdentifier bool + subjectAttributes []x509_cert.SubjectTypeName +} + +// Option is an interface for a function in the options pattern. +type Option = func(*issueOptions) + +var defaultIssueOptions = &issueOptions{ + includePermanentIdentifier: false, + subjectAttributes: []x509_cert.SubjectTypeName{}, +} + +func Issue(chain []*x509.Certificate, key *rsa.PrivateKey, subject string, optionFns ...Option) (*vc.VerifiableCredential, error) { + options := defaultIssueOptions + for _, fn := range optionFns { + fn(options) + } + + types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName} + if options.includePermanentIdentifier { + types = append(types, x509_cert.SanTypePermanentIdentifierValue) + types = append(types, x509_cert.SanTypePermanentIdentifierAssigner) + } + + issuer, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...) + 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") + } + otherNameValues, err := x509_cert.FindSanTypes(signingCert) + if err != nil { + return nil, err + } + subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, options.subjectAttributes...) + if err != nil { + return nil, err + } + template, err := buildCredential(*issuer, signingCert.NotAfter, otherNameValues, subjectTypes, subject) + if err != nil { + return nil, err + } + return vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { + token, err := convertClaims(claims) + if err != nil { + return "", err + } + hdrs, err := convertHeaders(headers) + if err != nil { + return "", err + } + + if hdrs.KeyID() == "" { + err := hdrs.Set("kid", issuer.String()+"#0") + if err != nil { + return "", err + } + } + + // x5c + serializedCert, err := marshalChain(chain...) + if err != nil { + return "", err + } + err = hdrs.Set("x5c", serializedCert) + if err != nil { + return "", err + } + + // x5t + hashSha1 := sha1.Sum(signingCert.Raw) + err = hdrs.Set("x5t", base64.RawURLEncoding.EncodeToString(hashSha1[:])) + if err != nil { + return "", err + } + + sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS256, *key, jws.WithProtectedHeaders(hdrs))) + return string(sign), err + }) +} + +// SubjectAttributes sets the subject attributes to include in the UZI VC. +func SubjectAttributes(attributes ...x509_cert.SubjectTypeName) Option { + return func(o *issueOptions) { + o.subjectAttributes = attributes + } +} + +// 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) { + chainPems := &cert.Chain{} + for _, certificate := range certificates { + err := chainPems.Add([]byte(base64.StdEncoding.EncodeToString(certificate.Raw))) + if err != nil { + return nil, err + } + } + headers, err := internal.FixChainHeaders(chainPems) + return headers, err +} + +// convertClaims converts a map of claims to a JWT token. +func convertClaims(claims map[string]interface{}) (jwt.Token, error) { + t := jwt.New() + for k, v := range claims { + if err := t.Set(k, v); err != nil { + return nil, err + } + } + return t, nil +} + +// convertHeaders converts a map of headers to jws.Headers, returning an error if any header fails to set. +func convertHeaders(headers map[string]interface{}) (jws.Headers, error) { + hdr := jws.NewHeaders() + + for k, v := range headers { + if err := hdr.Set(k, v); err != nil { + return nil, err + } + } + return hdr, nil +} + +func buildCredential(issuerDID did.DID, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID string) (*vc.VerifiableCredential, error) { + iat := time.Now() + subject := map[string]interface{}{ + "id": subjectDID, + } + addSubjectPolicyProperty := func(policy string, propKey string, propValue string) { + policyProps, ok := subject[policy].(map[string]interface{}) + if !ok { + policyProps = make(map[string]interface{}) + subject[policy] = policyProps + } + policyProps[propKey] = propValue + } + for _, otherNameValue := range otherNameValues { + addSubjectPolicyProperty(string(otherNameValue.PolicyType), string(otherNameValue.Type), otherNameValue.Value) + } + + for _, subjectType := range subjectTypes { + addSubjectPolicyProperty(string(subjectType.PolicyType), string(subjectType.Type), subjectType.Value) + } + + id := did.DIDURL{ + DID: issuerDID, + Fragment: uuid.NewString(), + }.URI() + return &vc.VerifiableCredential{ + Issuer: issuerDID.URI(), + Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, + Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), CredentialType}, + ID: &id, + IssuanceDate: iat, + ExpirationDate: &expirationDate, + CredentialSubject: []interface{}{subject}, + }, nil +} diff --git a/credential_issuer/issuer_test.go b/credential_issuer/issuer_test.go new file mode 100644 index 0000000..300b5a5 --- /dev/null +++ b/credential_issuer/issuer_test.go @@ -0,0 +1,131 @@ +package credential_issuer + +import ( + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "github.com/nuts-foundation/go-didx509-toolkit/internal" + "testing" + + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildX509Credential(t *testing.T) { + allCerts, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain)) + require.NoError(t, err) + chain, err := internal.ParseCertificateChain(allCerts) + require.NoError(t, err) + + privKey, err := internal.ParseRSAPrivateKeyFromPEM([]byte(internal.TestSigningKey)) + require.NoError(t, err, "failed to read signing key") + + type inFn = func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) + + defaultIn := func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { + + return chain, privKey, "did:example:123" + } + + tests := []struct { + name string + in inFn + errorText string + }{ + { + name: "ok - valid chain", + in: defaultIn, + errorText: "", + }, + { + name: "nok - empty serial number", + in: func(*testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { + certs, privKey, didStr := defaultIn(t) + certs[0].Subject.SerialNumber = "" + return certs, privKey, didStr + }, + errorText: "serialNumber not found in signing certificate", + }, + { + name: "nok - invalid signing certificate 2", + in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { + certs, privKey, didStr := defaultIn(t) + + certs[0].ExtraExtensions = make([]pkix.Extension, 0) + certs[0].Extensions = make([]pkix.Extension, 0) + return certs, privKey, didStr + }, + errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", + }, + { + name: "nok - empty cert in chain", + in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { + certs, privKey, didStr := defaultIn(t) + certs[0] = &x509.Certificate{} + return certs, privKey, didStr + }, + errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + certificates, signingKey, subject := tt.in(t) + _, err := Issue(certificates, signingKey, subject) + if err != nil { + if err.Error() != tt.errorText { + t.Errorf("TestBuildX509Credential() error = '%v', wantErr '%v'", err.Error(), tt.errorText) + } + } else if err == nil && tt.errorText != "" { + t.Errorf("TestBuildX509Credential() unexpected success, want error") + } + }) + } +} + +func TestIssue(t *testing.T) { + validKey, err := internal.ParseRSAPrivateKeyFromPEM([]byte(internal.TestSigningKey)) + require.NoError(t, err, "failed to parse signing key") + t.Run("ok - happy path", func(t *testing.T) { + validChain, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain)) + require.NoError(t, err, "failed to parse chain") + + vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization)) + + require.NoError(t, err, "failed to issue verifiable credential") + require.NotNil(t, vc, "verifiable credential is nil") + + assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String()) + assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential"))) + assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential"))) + assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare", vc.Issuer.String()) + + expectedCredentialSubject := []interface{}{map[string]interface{}{ + "id": "did:example:123", + "subject": map[string]interface{}{ + "O": "FauxCare", + }, + "san": map[string]interface{}{ + "otherName": "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333", + "permanentIdentifier.assigner": "2.16.528.1.1007.3.3", + "permanentIdentifier.value": "2222222", + }, + }} + + assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject) + assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate, "expiration date of VC must match signing certificate") + }) + + t.Run("ok - correct escaping of special characters", func(t *testing.T) { + validChain, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain)) + require.NoError(t, err) + + validChain[0].Subject.Organization = []string{"FauxCare & Co"} + + vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization)) + + assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare%20%26%20Co", vc.Issuer.String()) + }) +} diff --git a/uzi_vc_validator/ura_validator.go b/credential_verifier/verifier.go similarity index 80% rename from uzi_vc_validator/ura_validator.go rename to credential_verifier/verifier.go index 56df085..38562df 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/credential_verifier/verifier.go @@ -1,4 +1,4 @@ -package uzi_vc_validator +package credential_verifier import ( "crypto/sha1" @@ -10,24 +10,12 @@ import ( "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" "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" - "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" + "github.com/nuts-foundation/go-didx509-toolkit/credential_issuer" + "github.com/nuts-foundation/go-didx509-toolkit/did_x509" + "github.com/nuts-foundation/go-didx509-toolkit/internal" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" ) -type UraValidator interface { - Validate(jwtString []byte) bool -} - -type UraValidatorImpl struct { - allowUziTestCa bool - allowSelfSignedCa bool -} - -func NewUraValidator(allowUziTestCa bool, allowSelfSignedCa bool) *UraValidatorImpl { - return &UraValidatorImpl{allowUziTestCa, allowSelfSignedCa} -} - type JwtHeaderValues struct { X509CertThumbprint string X509CertChain *cert.Chain @@ -35,11 +23,20 @@ type JwtHeaderValues struct { Algorithm jwa.SignatureAlgorithm } -func (u UraValidatorImpl) Validate(jwtString string) error { +// Verify parses the given Verifiable Credential and checks whether it's a valid X509Credential: +// - It checks if the credential is of type X509Credential. +// - It checks whether the credential issuer is a valid did:x509 DID. +// - It checks whether the credential proof if valid. +// - It verifies the did:x509 policies. +// Note: it does NOT check whether the Verifiable Credential subject only contains fields from the did:x509 policies! +func Verify(jwtString string) error { credential, err := vc.ParseVerifiableCredential(jwtString) if err != nil { return err } + if !credential.IsType(credential_issuer.CredentialType) { + return fmt.Errorf("credential is not of type %s", credential_issuer.CredentialType) + } parseDid, err := did_x509.ParseDid(credential.Issuer.String()) if err != nil { return err @@ -59,7 +56,7 @@ func (u UraValidatorImpl) Validate(jwtString string) error { return err } - err = validateChain(signingCert, chainCertificates, u.allowUziTestCa, u.allowSelfSignedCa) + err = validateChain(signingCert, chainCertificates) if err != nil { return err } @@ -88,7 +85,6 @@ func (u UraValidatorImpl) Validate(jwtString string) error { if !found { return fmt.Errorf("unable to locate a value for %s of policy %s", policy.Type, policy.PolicyType) } - } return nil } @@ -138,30 +134,18 @@ func checkForOtherNamePolicy(otherNames []*x509_cert.OtherNameValue, policy *x50 return false, nil } -// func validateChain(signingCert *x509.Certificate, certificates []*x509.Certificate, includeTest 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 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(allowUziTestCa) - if err != nil { - return err - } - } - err = validate(signingCert, roots, intermediates) +func validateChain(signingCert *x509.Certificate, chain []*x509.Certificate) error { + parsedChain, err := internal.ParseCertificateChain(chain) if err != nil { - err = fmt.Errorf("could not validate against the CA pool. %s", err.Error()) return err } - return nil + roots := x509.NewCertPool() + roots.AddCert(parsedChain[len(parsedChain)-1]) + intermediates := x509.NewCertPool() + for i := 1; i < len(parsedChain)-1; i++ { + intermediates.AddCert(parsedChain[i]) + } + return validate(signingCert, roots, intermediates) } func validate(signingCert *x509.Certificate, roots *x509.CertPool, intermediates *x509.CertPool) error { diff --git a/did_x509/did_x509.go b/did_x509/did_x509.go index f1acf7a..3cb65a5 100644 --- a/did_x509/did_x509.go +++ b/did_x509/did_x509.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" "github.com/nuts-foundation/go-did/did" - "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" "net/url" "regexp" "strings" @@ -47,14 +47,14 @@ func CreateDid(signingCert, caCert *x509.Certificate, subjectAttributes []x509_c if err != nil { return nil, err } - policies := CreateOtherNamePolicies(otherNames) + policies := createOtherNamePolicies(otherNames) subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...) if err != nil { return nil, err } - policies = append(policies, CreateSubjectPolicies(subjectTypes)...) + policies = append(policies, createSubjectPolicies(subjectTypes)...) formattedDid, err := FormatDid(caCert, policies...) return formattedDid, err @@ -127,9 +127,9 @@ func ParseDid(didString string) (*X509Did, error) { return &x509Did, nil } -// CreateOtherNamePolicies constructs a policy string using the provided URA, fixed string "san", and "permanentIdentifier". +// createOtherNamePolicies constructs a policy string using the provided URA, fixed string "san", and "permanentIdentifier". // It joins these components with colons and returns the resulting policy string. -func CreateOtherNamePolicies(otherNames []*x509_cert.OtherNameValue) []string { +func createOtherNamePolicies(otherNames []*x509_cert.OtherNameValue) []string { var policies []string for _, otherName := range otherNames { value := PercentEncode(otherName.Value) @@ -140,7 +140,7 @@ func CreateOtherNamePolicies(otherNames []*x509_cert.OtherNameValue) []string { return policies } -func CreateSubjectPolicies(subjectValues []*x509_cert.SubjectValue) []string { +func createSubjectPolicies(subjectValues []*x509_cert.SubjectValue) []string { var policies []string for _, subjectValue := range subjectValues { value := PercentEncode(subjectValue.Value) @@ -150,13 +150,3 @@ func CreateSubjectPolicies(subjectValues []*x509_cert.SubjectValue) []string { } return policies } - -// 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 x509_cert.IsRootCa(cert) { - return cert, nil - } - } - return nil, fmt.Errorf("cannot find root certificate") -} diff --git a/did_x509/did_x509_test.go b/did_x509/did_x509_test.go index 657edb9..20e9d9d 100644 --- a/did_x509/did_x509_test.go +++ b/did_x509/did_x509_test.go @@ -3,13 +3,14 @@ package did_x509 import ( "crypto/x509" "encoding/base64" + "github.com/nuts-foundation/go-didx509-toolkit/internal" "github.com/stretchr/testify/require" "net/url" "strings" "testing" "github.com/nuts-foundation/go-did/did" - "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" "github.com/stretchr/testify/assert" ) @@ -47,7 +48,7 @@ func TestCreateDidSingle(t *testing.T) { type args struct { chain []*x509.Certificate } - chain, _, rootCert, _, _, err := x509_cert.BuildSelfSignedCertChain("A_BIG_STRING", "A_PERMANENT_STRING") + chain, _, rootCert, _, _, err := internal.BuildSelfSignedCertChain("A_BIG_STRING", "A_PERMANENT_STRING") if err != nil { t.Fatal(err) } @@ -134,7 +135,7 @@ func TestCreateDidDouble(t *testing.T) { type args struct { chain []*x509.Certificate } - chain, _, rootCert, _, _, err := x509_cert.BuildSelfSignedCertChain("A_BIG_STRING", "A_SMALL_STRING") + chain, _, rootCert, _, _, err := internal.BuildSelfSignedCertChain("A_BIG_STRING", "A_SMALL_STRING") if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 9f9906a..e2d53ac 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/nuts-foundation/uzi-did-x509-issuer +module github.com/nuts-foundation/go-didx509-toolkit go 1.23.1 @@ -8,7 +8,6 @@ require ( github.com/lestrrat-go/jwx/v2 v2.1.2 github.com/nuts-foundation/go-did v0.15.0 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.31.0 ) require ( @@ -27,6 +26,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shengdoushi/base58 v1.0.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/constants.go b/internal/constants.go new file mode 100644 index 0000000..baa4393 --- /dev/null +++ b/internal/constants.go @@ -0,0 +1,12 @@ +package internal + +import ( + "encoding/asn1" +) + +var ( + // SubjectAlternativeNameType represents the ASN.1 Object Identifier for Subject Alternative Name. + SubjectAlternativeNameType = asn1.ObjectIdentifier{2, 5, 29, 17} + PermanentIdentifierType = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3} + OtherNameType = asn1.ObjectIdentifier{2, 5, 5, 5} +) diff --git a/x509_cert/x509_test_utils.go b/internal/test_ca.go similarity index 64% rename from x509_cert/x509_test_utils.go rename to internal/test_ca.go index 8faabeb..37b4e28 100644 --- a/x509_cert/x509_test_utils.go +++ b/internal/test_ca.go @@ -1,42 +1,30 @@ -package x509_cert +package internal import ( - "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" - "fmt" "math/big" "time" "github.com/lestrrat-go/jwx/v2/cert" ) -const ( - CertificateBlockType = "CERTIFICATE" - RSAPrivKeyBlockType = "PRIVATE KEY" -) +var permanentIdentifierAssigner = asn1.ObjectIdentifier{2, 16, 528, 1, 1007, 3, 3} +var subjectAlternativeNameType = asn1.ObjectIdentifier{2, 5, 29, 17} +var otherNameType = asn1.ObjectIdentifier{2, 5, 5, 5} -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 +type otherName struct { + TypeID asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"tag:0,explicit"` } -func EncodeCertificates(certs ...*x509.Certificate) ([]byte, error) { - b := bytes.Buffer{} - for _, c := range certs { - if err := pem.Encode(&b, &pem.Block{Type: CertificateBlockType, Bytes: c.Raw}); err != nil { - return []byte{}, err - } - } - return b.Bytes(), nil +type stringAndOid struct { + Value string + Assigner asn1.ObjectIdentifier } // BuildSelfSignedCertChain generates a certificate chain, including root, intermediate, and signing certificates. @@ -45,11 +33,11 @@ func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string if err != nil { return nil, nil, nil, nil, nil, err } - rootCertTmpl, err := CertTemplate(nil, "Root CA") + rootCertTmpl, err := certTemplate(nil, "Root CA") if err != nil { return nil, nil, nil, nil, nil, err } - rootCert, rootPem, err := CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) + rootCert, rootPem, err := createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) if err != nil { return nil, nil, nil, nil, nil, err } @@ -58,11 +46,11 @@ func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL1Tmpl, err := CertTemplate(nil, "Intermediate CA Level 1") + intermediateL1Tmpl, err := certTemplate(nil, "Intermediate CA Level 1") if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL1Cert, intermediateL1Pem, err := CreateCert(intermediateL1Tmpl, rootCertTmpl, &intermediateL1Key.PublicKey, rootKey) + intermediateL1Cert, intermediateL1Pem, err := createCert(intermediateL1Tmpl, rootCertTmpl, &intermediateL1Key.PublicKey, rootKey) if err != nil { return nil, nil, nil, nil, nil, err } @@ -71,11 +59,11 @@ func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL2Tmpl, err := CertTemplate(nil, "Intermediate CA Level 2") + intermediateL2Tmpl, err := certTemplate(nil, "Intermediate CA Level 2") if err != nil { return nil, nil, nil, nil, nil, err } - intermediateL2Cert, intermediateL2Pem, err := CreateCert(intermediateL2Tmpl, intermediateL1Cert, &intermediateL2Key.PublicKey, intermediateL1Key) + intermediateL2Cert, intermediateL2Pem, err := createCert(intermediateL2Tmpl, intermediateL1Cert, &intermediateL2Key.PublicKey, intermediateL1Key) if err != nil { return nil, nil, nil, nil, nil, err } @@ -84,11 +72,11 @@ func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string if err != nil { return nil, nil, nil, nil, nil, err } - signingTmpl, err := SigningCertTemplate(nil, identifier, permanentIdentifierValue) + signingTmpl, err := signingCertTemplate(nil, identifier, permanentIdentifierValue) if err != nil { return nil, nil, nil, nil, nil, err } - signingCert, signingPEM, err := CreateCert(signingTmpl, intermediateL2Cert, &signingKey.PublicKey, intermediateL2Key) + signingCert, signingPEM, err := createCert(signingTmpl, intermediateL2Cert, &signingKey.PublicKey, intermediateL2Key) if err != nil { return nil, nil, nil, nil, nil, err } @@ -110,9 +98,9 @@ func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string 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. +// 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, organization string) (*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) @@ -133,8 +121,8 @@ func CertTemplate(serialNumber *big.Int, organization string) (*x509.Certificate return &tmpl, nil } -// SigningCertTemplate creates a x509.Certificate template for a signing certificate with an optional serial number. -func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIdentifierValue string) (*x509.Certificate, error) { +// signingCertTemplate creates a x509.Certificate template for a signing certificate with an optional serial number. +func signingCertTemplate(serialNumber *big.Int, identifier string, permanentIdentifierValue 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) @@ -144,8 +132,8 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIden if err != nil { return nil, err } - otherName := OtherName{ - TypeID: OtherNameType, + identifierOtherName := otherName{ + TypeID: otherNameType, Value: asn1.RawValue{ Class: 2, Tag: 0, @@ -154,7 +142,7 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIden }, } - raw, err = toRawValue(otherName, "tag:0") + raw, err = toRawValue(identifierOtherName, "tag:0") if err != nil { return nil, err } @@ -162,15 +150,15 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIden list = append(list, *raw) if permanentIdentifierValue != "" { - permId := StingAndOid{ + permId := stringAndOid{ Value: permanentIdentifierValue, - Assigner: UraAssigner, + Assigner: permanentIdentifierAssigner, } raw, err = toRawValue(permId, "seq") if err != nil { return nil, err } - permOtherName := OtherName{ + permOtherName := otherName{ TypeID: PermanentIdentifierType, Value: asn1.RawValue{ Class: 2, @@ -185,12 +173,10 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIden } list = append(list, *raw) } - //fmt.Println("OFF") marshal, err := asn1.Marshal(list) if err != nil { return nil, err } - //err = DebugUnmarshall(marshal, 0) tmpl := x509.Certificate{ SerialNumber: serialNumber, @@ -202,18 +188,12 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIden BasicConstraintsValid: true, ExtraExtensions: []pkix.Extension{ { - Id: SubjectAlternativeNameType, + Id: subjectAlternativeNameType, Critical: false, Value: marshal, }, }, } - uzi, _, _, err := ParseUraFromOtherNameValue(identifier) - if err != nil { - // Crate an incorrect uzi in order to test invalid UZI numbers - uzi = "9876543212" - } - tmpl.Subject.SerialNumber = uzi tmpl.KeyUsage = x509.KeyUsageDigitalSignature tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} return &tmpl, nil @@ -233,10 +213,9 @@ func toRawValue(identifier any, tag string) (*asn1.RawValue, error) { return &val, nil } -// CreateCert generates a new x509 certificate using the provided template and parent certificates, public and private keys. +// createCert generates a new x509 certificate using the provided template and parent certificates, public and private keys. // It returns the generated certificate, its PEM-encoded version, and any error encountered during the process. -func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, certPEM []byte, err error) { - +func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, certPEM []byte, err error) { certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv) if err != nil { return nil, nil, err @@ -251,56 +230,3 @@ func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv certPEM = pem.EncodeToMemory(&b) return cert, certPEM, err } - -// 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 - tail, err := asn1.Unmarshal(data, &x) - if err != nil { - return err - } - prefix := "" - for i := 0; i < depth; i++ { - prefix += "\t" - } - fmt.Printf("%sUnmarshalled: compound: %t, tag: %d, class: %d", prefix, x.IsCompound, x.Tag, x.Class) - - if x.Bytes != nil { - if x.IsCompound || x.Tag == 0 { - fmt.Println() - err := DebugUnmarshall(x.Bytes, depth+1) - if err != nil { - return err - } - } else { - switch x.Tag { - case asn1.TagBoolean: - fmt.Printf(", value boolean: %v", x.Bytes) - case asn1.TagOID: - fmt.Printf(", value: OID: %v", x.Bytes) - case asn1.TagInteger: - fmt.Printf(", value: integer: %v", x.Bytes) - case asn1.TagUTF8String: - fmt.Printf(", value: bitstring: %v", x.Bytes) - case asn1.TagBitString: - fmt.Printf(", value: bitstring: %v", x.Bytes) - case asn1.TagOctetString: - fmt.Printf(", value: octetstring: %v", x.Bytes) - case asn1.TagIA5String: - fmt.Printf(", value: TagIA5String: %v", x.Bytes) - case asn1.TagNull: - fmt.Printf(", value: null") - default: - return fmt.Errorf("unknown tag: %d", x.Tag) - - } - fmt.Println() - } - } - data = tail - } - - return nil -} diff --git a/uzi_vc_issuer/testdata/valid_chain.pem b/internal/test_certs.go similarity index 72% rename from uzi_vc_issuer/testdata/valid_chain.pem rename to internal/test_certs.go index db2f6fa..3908633 100644 --- a/uzi_vc_issuer/testdata/valid_chain.pem +++ b/internal/test_certs.go @@ -1,3 +1,35 @@ +package internal + +const TestSigningKey = ` +-----BEGIN PRIVATE KEY----- +MIIEpAIBAAKCAQEA0+LyiNeyaWUCHyzRDKG6+a+btQXCiuKX7cRzLWRp3Dc0v4Bv +8GCgNNSHRwHz+KbadpGuBebvD6f4GvwDA9PjdJ3xjjRi0aF1hYmzPmnrsYxDtuZV +A1IU0BTBbfQcQ9yXpUEtPZwDvf3/J9OlckNrEVOKZzG7Bw/0rk4OGt4D4lOkmhlg +yvWHou59J9/ARrRaRl1b5bcjzrUnEbD6B8w3G6xino57/M2cvExFu3dChV6eABDD +hwE+WsSK2CaijXMiCD25xg80nq9gzTIJEVlSEYilf4DthdyaRae4WV74/x5H3EZI +wRHr+8A2rZmNXivpUuDXAtTH5BsJibujfeYfJQIDAQABAoIBAQCouZC+bVyR1rBA +2PRC5cq5JyCLnuGSrOukl4nL/Kjbhk6HrCP3O0p3p0Ftxt1bBKr0Pf9gjcuSIQRN +oJ5Z/vGiHF+NCKQkIDkwND26lqfrwzDsxS+vLD6Mj+qTvw5+73sGSgdXhxPnyAnV +0hBuE8d/jZGpqQ0wi4EhB+DtfhuDrfp4ZdQXFynsTi/MuttOmiu4r/aVqYS4uhjh +NO4EvnfDpyUVxpdy78t8H16+ghyAI5fnELqxq7Gk1EIrFYtUfnEfJKyB33ZLZrkL +imIVc5Qt90zObFFljDrM26cr7OnKopApO98dREi13aLio2QeKX3OkUOIlcGgwAN0 +Fhpvv7jBAoGBAOodh925HBKiIJlE3C5oYwnalVLnr7fVffACuCyxE2oxPv7jl1qP +XnJ1i5DIgwyqUBzLbgo6Jf9fVTK/0lzx0rMIDBTiMJE+YE7UZ1/BFsdql0AnkHP9 +S21E3vdVPl1vcQMbc9fokkGYayMDkLeToLKVIv/76IZkQ1iS6vpWwQqVAoGBAOex +eNmXms9FSx2aw3IdAHnS11u+cvn60hYmvVod10kFzhO2l17WBtlWSzysWchEWzX3 +u0bW95LIuRerlj463jqw89ecEadmOsn382Icpsf2ZhtswyeAIa7u0/y965xX6j0+ +dbx1GQ8Ftdt9nozb/o41sF5/DjrYWmQFmOrvfy5RAoGAY0/9p7/zua/O9lWwtXsQ +sEhqWc3wy6IkF2F/8W14l+6mE4hGV2NEJHfaqaN1fDTvYRem6W27WrZ9NNcMjOME +h2/deCpvgd2dCzOtWoBVgmikGtHtxFZp3cN+dhtSJl606SWHIcsF6A+ZOzQy+r0E +SV1ciIy7Ge+EZhmE1odgwnUCgYBatXa06c/oOh7Qdljygjw/dbZu6r8k83fwyDX1 +5Bz3L9igiyn0LSL9T/WgyXFVIL39AQJHF75Rr1gX1ku6DV4X6FNvJGEdAr8dd3/H +96OsQeFz9z7oZhfJ3yMLnmdyDFFerOd3YvjukrPCPQon57Ffh9GHDYNKso2g/zgB +Msa+IQKBgQDEpBtnP0+oJCAlUb3bOqJY7+5B4GsBCbNgV2rpa362Iyx5OPKLdGgV +U/G3jdZeqrzLJsFPfU41jOFiL76pDsH0rCUK1fQJkAhrux/k/5rYWhgfziZYwiNT +RccjKo7mMgg/vPjbw/wtLhvLTstfMQl5OLJ0f/vROR24ThAdNiFGWw== +-----END PRIVATE KEY----- +` +const TestCertificateChain = ` -----BEGIN CERTIFICATE----- MIIDjTCCAnWgAwIBAgICAMQwDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UEChMXSW50 ZXJtZWRpYXRlIENBIExldmVsIDIwHhcNMjQxMjAzMTQ1NTIyWhcNMjUwMTAyMTQ1 @@ -78,3 +110,4 @@ NBzSu6ED5rdaBV68oc/QrCPhorbHVgOUq0TBjniqtNAYOTbtK7nsult6Xf5hlFaS +KkbO3eIgkiINOBDBZg6c9TUYIu+SP9/27j60xoHjumEmayi94smeJAQ5qqkjTY6 O6Qln1c= -----END CERTIFICATE----- +` diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..71319c8 --- /dev/null +++ b/internal/util.go @@ -0,0 +1,142 @@ +package internal + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "github.com/lestrrat-go/jwx/v2/cert" + "strings" +) + +// FixChainHeaders replaces newline characters in the certificate chain headers with escaped newline sequences. +// It processes each certificate in the provided chain and returns a new chain with the modified headers or an error if any occurs. +func FixChainHeaders(chain *cert.Chain) (*cert.Chain, error) { + rv := &cert.Chain{} + for i := 0; i < chain.Len(); i++ { + value, _ := chain.Get(i) + der := strings.ReplaceAll(string(value), "\n", "\\n") + err := rv.AddString(der) + if err != nil { + return nil, err + } + } + return rv, nil +} + +// ParseCertificatesFromPEM reads a list of certificates from the given data. +func ParseCertificatesFromPEM(data []byte) ([]*x509.Certificate, error) { + pemBlocks, err := parsePemBytes(data) + if err != nil { + return nil, err + } + + certs := make([]*x509.Certificate, 0) + for _, block := range pemBlocks { + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, c) + } + return certs, nil +} + +// ParseCertificateChain constructs a valid 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. +// 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 ParseCertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) { + var signingCert *x509.Certificate + for _, c := range certs { + if c != nil && !c.IsCA { + signingCert = c + break + } + } + if signingCert == nil { + return nil, errors.New("failed to find signing certificate") + } + + var chain []*x509.Certificate + chain = append(chain, signingCert) + + certToCheck := signingCert + for !isRootCa(certToCheck) { + found := false + for _, c := range certs { + if c == nil || c.Equal(signingCert) { + continue + } + err := certToCheck.CheckSignatureFrom(c) + if err == nil { + chain = append(chain, c) + certToCheck = c + found = true + break + } + } + if !found { + return nil, errors.New("failed to find path from signingCert to root") + } + } + return chain, nil +} + +// ParseRSAPrivateKeyFromPEM reads a RSA private key from the given data. +// It returns an error if the key cannot be parsed. +func ParseRSAPrivateKeyFromPEM(data []byte) (*rsa.PrivateKey, error) { + pemBlocks, err := parsePemBytes(data) + if err != nil { + return nil, err + } + if len(pemBlocks) != 1 { + return nil, errors.New("expected exactly one PEM block") + } + return newRSAPrivateKey(pemBlocks[0]) +} + +// newRSAPrivateKey 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 newRSAPrivateKey(block *pem.Block) (*rsa.PrivateKey, error) { + if block.Type != "PRIVATE KEY" && block.Type != "RSA PRIVATE KEY" { + return nil, errors.New("expected PEM block type to be PRIVATE KEY or RSA PRIVATE KEY") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + } + if _, ok := key.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("key is not RSA") + } + return key.(*rsa.PrivateKey), err +} + +// parsePemBytes parses a nonEmptyBytes slice into a pemBlocks +// it returns an error if the input does not contain any PEM blocks. +func parsePemBytes(f []byte) ([]*pem.Block, error) { + blocks := make([]*pem.Block, 0) + for { + block, rest := pem.Decode(f) + if block == nil { + break + } + blocks = append(blocks, block) + f = rest + } + + if len(blocks) == 0 { + return nil, errors.New("no PEM blocks found") + } + + return blocks, nil +} + +func isRootCa(signingCert *x509.Certificate) bool { + return signingCert.IsCA && bytes.Equal(signingCert.RawIssuer, signingCert.RawSubject) +} diff --git a/internal/util_test.go b/internal/util_test.go new file mode 100644 index 0000000..77a9b20 --- /dev/null +++ b/internal/util_test.go @@ -0,0 +1,133 @@ +package internal + +import ( + "crypto/x509" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParsePemBytes(t *testing.T) { + tests := []struct { + name string + pemBytes []byte + expectNumBlocks int + expectErr bool + }{ + {"ValidChain", []byte(TestCertificateChain), 4, false}, + {"InvalidChain", []byte("invalid pem"), 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + blocks, err := parsePemBytes(tt.pemBytes) + if (err != nil) != tt.expectErr { + t.Errorf("parsePemBytes() error = %v, expectErr %v", err, tt.expectErr) + } + + if len(blocks) != tt.expectNumBlocks { + t.Errorf("parsePemBytes() = %v, want %v", len(blocks), tt.expectNumBlocks) + } + }) + } +} + +func TestParseCertificateChain(t *testing.T) { + certs, err := ParseCertificatesFromPEM([]byte(TestCertificateChain)) + require.NoError(t, err) + + tests := []struct { + name string + errorText string + in func(certs []*x509.Certificate) []*x509.Certificate + out func(certs []*x509.Certificate) []*x509.Certificate + }{ + { + name: "ok - valid cert input", + in: func(certs []*x509.Certificate) []*x509.Certificate { + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return certs + }, + errorText: "", + }, + { + name: "ok - it handles out of order certificates", + in: func(certs []*x509.Certificate) []*x509.Certificate { + certs = []*x509.Certificate{certs[2], certs[0], certs[3], certs[1]} + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return certs + }, + errorText: "", + }, + { + name: "nok - missing signing certificate", + in: func(certs []*x509.Certificate) []*x509.Certificate { + certs = certs[1:] + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return nil + }, + errorText: "failed to find signing certificate", + }, + { + name: "nok - missing root CA certificate", + in: func(certs []*x509.Certificate) []*x509.Certificate { + certs = certs[:3] + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return nil + }, + errorText: "failed to find path from signingCert to root", + }, + { + name: "nok - missing first intermediate CA certificate", + in: func(certs []*x509.Certificate) []*x509.Certificate { + certs = []*x509.Certificate{certs[0], certs[2], certs[3]} + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return nil + }, + errorText: "failed to find path from signingCert to root", + }, + { + name: "nok - missing second intermediate CA certificate", + in: func(certs []*x509.Certificate) []*x509.Certificate { + certs = []*x509.Certificate{certs[0], certs[1], certs[3]} + return certs + }, + out: func(certs []*x509.Certificate) []*x509.Certificate { + return nil + }, + errorText: "failed to find path from signingCert to root", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inputCerts := tt.in(certs) + expectedCerts := tt.out(certs) + resultCerts, err := ParseCertificateChain(inputCerts) + if err != nil { + if err.Error() != tt.errorText { + t.Errorf("BuildCertificateChain() error = '%v', wantErr '%v'", err.Error(), tt.errorText) + } + } else if err == nil && tt.errorText != "" { + t.Errorf("BuildCertificateChain() unexpected success, want error") + } + if len(resultCerts) != len(expectedCerts) { + t.Errorf("BuildCertificateChain() expected %d certificates, got %d", len(expectedCerts), len(resultCerts)) + return + } + for i := range resultCerts { + if !resultCerts[i].Equal(expectedCerts[i]) { + t.Errorf("BuildCertificateChain() at index %d expected %v, got %v", i, expectedCerts[i], resultCerts[i]) + } + } + }) + } +} diff --git a/main.go b/main.go index 9e6cf16..a1b2541 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,9 @@ import ( "bufio" "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" + "github.com/nuts-foundation/go-didx509-toolkit/credential_issuer" + "github.com/nuts-foundation/go-didx509-toolkit/internal" + "github.com/nuts-foundation/go-didx509-toolkit/x509_cert" "os" ) @@ -14,21 +15,12 @@ type VC struct { 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."` SubjectAttributes []x509_cert.SubjectTypeName `short:"s" name:"subject_attr" help:"A list of Subject Attributes u in the VC." default:"O,L"` - Test bool `short:"t" help:"Allow for certificates signed by the TEST UZI Root CA."` IncludePermanent bool `short:"p" help:"Include the permanent identifier in the did:x509."` } -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."` - TestCert TestCert `cmd:"" help:"Create a new test certificate."` + Version string `help:"Show version."` + Vc VC `cmd:"" help:"Create a new VC."` } func main() { @@ -55,55 +47,6 @@ func main() { fmt.Println(err) os.Exit(-1) } - 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- - 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.BuildSelfSignedCertChain(otherName, cli.TestCert.Ura) - 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) - } - err = printLineAndFlush(jwt) - if err != nil { - fmt.Println(err) - os.Exit(-1) - } default: fmt.Println("Unknown command") os.Exit(-1) @@ -123,24 +66,29 @@ func printLineAndFlush(jwt string) error { } func issueVc(vc VC) (string, error) { - chain, err := uzi_vc_issuer.NewValidCertificateChain(vc.CertificateFile) + certFileData, err := os.ReadFile(vc.CertificateFile) + if err != nil { + return "", fmt.Errorf("failed to read certificate file: %w", err) + } + certs, err := internal.ParseCertificatesFromPEM(certFileData) if err != nil { return "", err } - - key, err := uzi_vc_issuer.NewPrivateKey(vc.SigningKey) + chain, err := internal.ParseCertificateChain(certs) if err != nil { return "", err } - subject, err := uzi_vc_issuer.NewSubjectDID(vc.SubjectDID) + keyFileData, err := os.ReadFile(vc.SigningKey) + if err != nil { + return "", fmt.Errorf("failed to read key file: %w", err) + } + key, err := internal.ParseRSAPrivateKeyFromPEM(keyFileData) if err != nil { return "", err } - credential, err := uzi_vc_issuer.Issue(chain, key, subject, - uzi_vc_issuer.SubjectAttributes(vc.SubjectAttributes...), - uzi_vc_issuer.AllowTestUraCa(vc.Test)) + credential, err := credential_issuer.Issue(chain, key, vc.SubjectDID, credential_issuer.SubjectAttributes(vc.SubjectAttributes...)) if err != nil { return "", err diff --git a/test_ca/README.md b/test_ca/README.md index fef2337..509501b 100644 --- a/test_ca/README.md +++ b/test_ca/README.md @@ -7,5 +7,5 @@ To issue a new fake UZI certificate, you can use the following command: You can then use the credential issuance tool (given you've run `go build .` in the parent directory) to generate a Verifiable Credential: ```bash - ../uzi-did-x509-issuer vc test_ca/out/-chain.pem test_ca/out/.key + ../go-didx509-toolkit vc test_ca/out/-chain.pem test_ca/out/.key ``` \ No newline at end of file diff --git a/uzi_vc_issuer/testdata/signing_key.pem b/uzi_vc_issuer/testdata/signing_key.pem deleted file mode 100644 index 8ce4f05..0000000 --- a/uzi_vc_issuer/testdata/signing_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEpAIBAAKCAQEA0+LyiNeyaWUCHyzRDKG6+a+btQXCiuKX7cRzLWRp3Dc0v4Bv -8GCgNNSHRwHz+KbadpGuBebvD6f4GvwDA9PjdJ3xjjRi0aF1hYmzPmnrsYxDtuZV -A1IU0BTBbfQcQ9yXpUEtPZwDvf3/J9OlckNrEVOKZzG7Bw/0rk4OGt4D4lOkmhlg -yvWHou59J9/ARrRaRl1b5bcjzrUnEbD6B8w3G6xino57/M2cvExFu3dChV6eABDD -hwE+WsSK2CaijXMiCD25xg80nq9gzTIJEVlSEYilf4DthdyaRae4WV74/x5H3EZI -wRHr+8A2rZmNXivpUuDXAtTH5BsJibujfeYfJQIDAQABAoIBAQCouZC+bVyR1rBA -2PRC5cq5JyCLnuGSrOukl4nL/Kjbhk6HrCP3O0p3p0Ftxt1bBKr0Pf9gjcuSIQRN -oJ5Z/vGiHF+NCKQkIDkwND26lqfrwzDsxS+vLD6Mj+qTvw5+73sGSgdXhxPnyAnV -0hBuE8d/jZGpqQ0wi4EhB+DtfhuDrfp4ZdQXFynsTi/MuttOmiu4r/aVqYS4uhjh -NO4EvnfDpyUVxpdy78t8H16+ghyAI5fnELqxq7Gk1EIrFYtUfnEfJKyB33ZLZrkL -imIVc5Qt90zObFFljDrM26cr7OnKopApO98dREi13aLio2QeKX3OkUOIlcGgwAN0 -Fhpvv7jBAoGBAOodh925HBKiIJlE3C5oYwnalVLnr7fVffACuCyxE2oxPv7jl1qP -XnJ1i5DIgwyqUBzLbgo6Jf9fVTK/0lzx0rMIDBTiMJE+YE7UZ1/BFsdql0AnkHP9 -S21E3vdVPl1vcQMbc9fokkGYayMDkLeToLKVIv/76IZkQ1iS6vpWwQqVAoGBAOex -eNmXms9FSx2aw3IdAHnS11u+cvn60hYmvVod10kFzhO2l17WBtlWSzysWchEWzX3 -u0bW95LIuRerlj463jqw89ecEadmOsn382Icpsf2ZhtswyeAIa7u0/y965xX6j0+ -dbx1GQ8Ftdt9nozb/o41sF5/DjrYWmQFmOrvfy5RAoGAY0/9p7/zua/O9lWwtXsQ -sEhqWc3wy6IkF2F/8W14l+6mE4hGV2NEJHfaqaN1fDTvYRem6W27WrZ9NNcMjOME -h2/deCpvgd2dCzOtWoBVgmikGtHtxFZp3cN+dhtSJl606SWHIcsF6A+ZOzQy+r0E -SV1ciIy7Ge+EZhmE1odgwnUCgYBatXa06c/oOh7Qdljygjw/dbZu6r8k83fwyDX1 -5Bz3L9igiyn0LSL9T/WgyXFVIL39AQJHF75Rr1gX1ku6DV4X6FNvJGEdAr8dd3/H -96OsQeFz9z7oZhfJ3yMLnmdyDFFerOd3YvjukrPCPQon57Ffh9GHDYNKso2g/zgB -Msa+IQKBgQDEpBtnP0+oJCAlUb3bOqJY7+5B4GsBCbNgV2rpa362Iyx5OPKLdGgV -U/G3jdZeqrzLJsFPfU41jOFiL76pDsH0rCUK1fQJkAhrux/k/5rYWhgfziZYwiNT -RccjKo7mMgg/vPjbw/wtLhvLTstfMQl5OLJ0f/vROR24ThAdNiFGWw== ------END PRIVATE KEY----- diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go deleted file mode 100644 index 5079067..0000000 --- a/uzi_vc_issuer/ura_issuer.go +++ /dev/null @@ -1,442 +0,0 @@ -package uzi_vc_issuer - -import ( - "context" - "crypto/rsa" - "crypto/sha1" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "os" - "time" - - "github.com/nuts-foundation/go-did/did" - - "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/did_x509" - "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" -) - -// CredentialType holds the name of the X.509 credential type. -const CredentialType = "X509Credential" - -// filename represents a valid file name. The file must exist. -type fileName string - -// nonEmptyBytes represents a non-empty byte slice. -type nonEmptyBytes []byte - -// newFileName creates a new fileName from a string. It returns an error if the file does not exist. -func newFileName(name string) (fileName, error) { - if _, err := os.Stat(name); err != nil { - return fileName(""), err - } - - return fileName(name), nil -} - -// readFile reads a file and returns its content as nonEmptyBytes. It returns an error if the file does not exist or is empty. -func readFile(name fileName) (nonEmptyBytes, error) { - bytes, err := os.ReadFile(string(name)) - if err != nil { - return nil, err - } - if len(bytes) == 0 { - return nil, errors.New("file is empty") - } - return nonEmptyBytes(bytes), nil -} - -// pemBlocks represents a list of one or more PEM blocks. -type pemBlocks []*pem.Block - -// parsePemBytes parses a nonEmptyBytes slice into a pemBlocks -// it returns an error if the input does not contain any PEM blocks. -func parsePemBytes(f nonEmptyBytes) (pemBlocks, error) { - blocks := make([]*pem.Block, 0) - for { - block, rest := pem.Decode(f) - if block == nil { - break - } - blocks = append(blocks, block) - f = rest - } - - if len(blocks) == 0 { - return nil, errors.New("no PEM blocks found") - } - - return blocks, nil -} - -// parseCertificatesFromPemBlocks parses a list of PEM blocks into a list of x509.Certificate instances. -// It returns an error if any of the blocks cannot be parsed into a certificate. -func parseCertificatesFromPemBlocks(blocks pemBlocks) (certificateList, error) { - certs := make([]*x509.Certificate, 0) - for _, block := range blocks { - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - certs = append(certs, cert) - } - return certs, nil -} - -// certificateList represents a non empty slice of x509.Certificate instances. -type certificateList []*x509.Certificate - -// validCertificateChain represents a valid certificate chain. -type validCertificateChain certificateList -type privateKey *rsa.PrivateKey -type subjectDID string - -// issueOptions contains values for options for issuing a UZI VC. -type issueOptions struct { - allowTestUraCa bool - includePermanentIdentifier bool - subjectAttributes []x509_cert.SubjectTypeName -} - -// Option is an interface for a function in the options pattern. -type Option = func(*issueOptions) - -// X509Credential represents a JWT encoded X.509 credential. -type X509Credential string - -var defaultIssueOptions = &issueOptions{ - allowTestUraCa: false, - includePermanentIdentifier: false, - subjectAttributes: []x509_cert.SubjectTypeName{}, -} - -// NewValidCertificateChain reads a file and returns a valid certificate chain. -// It returns an error if the file does not exist or is empty or the certificates cannot be parsed or the chain is not valid. -func NewValidCertificateChain(fileName string) (validCertificateChain, error) { - certFileName, err := newFileName(fileName) - - if err != nil { - return nil, err - } - - fileBytes, err := readFile(certFileName) - if err != nil { - return nil, err - } - pemBlocks, err := parsePemBytes(fileBytes) - if err != nil { - return nil, err - } - - certs, err := parseCertificatesFromPemBlocks(pemBlocks) - if err != nil { - return nil, err - } - - chain, err := newCertificateChain(certs) - if err != nil { - return nil, err - } - - return chain, nil -} - -// NewPrivateKey reads a file and returns an RSA private key. -// It returns an error if the file does not exist or is empty or the key cannot be parsed. -func NewPrivateKey(fileName string) (privateKey, error) { - keyFileName, err := newFileName(fileName) - if err != nil { - return nil, err - } - - keyFileBytes, err := readFile(keyFileName) - if err != nil { - return nil, err - } - - keyBlocks, err := parsePemBytes(keyFileBytes) - if err != nil { - return nil, err - } - - key, err := newRSAPrivateKey(keyBlocks) - if err != nil { - return nil, err - } - - return key, nil -} - -func NewSubjectDID(didStr string) (subjectDID, error) { - subject, err := did.ParseDID(didStr) - if err != nil { - return "", err - } - return subjectDID(subject.String()), nil -} - -// newRSAPrivateKey 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 newRSAPrivateKey(pemBlocks pemBlocks) (privateKey, error) { - if len(pemBlocks) != 1 || pemBlocks[0].Type != "PRIVATE KEY" { - return nil, errors.New("expected exactly one private key block") - } - block := pemBlocks[0] - - key, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - key, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - } - - if _, ok := key.(*rsa.PrivateKey); !ok { - return nil, fmt.Errorf("key is not RSA") - } - return key.(*rsa.PrivateKey), err -} - -func Issue(chain validCertificateChain, key privateKey, subject subjectDID, optionFns ...Option) (*vc.VerifiableCredential, error) { - options := defaultIssueOptions - for _, fn := range optionFns { - fn(options) - } - - types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName} - if options.includePermanentIdentifier { - types = append(types, x509_cert.SanTypePermanentIdentifierValue) - types = append(types, x509_cert.SanTypePermanentIdentifierAssigner) - } - - issuer, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...) - 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") - } - otherNameValues, err := x509_cert.FindSanTypes(signingCert) - if err != nil { - return nil, err - } - subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, options.subjectAttributes...) - if err != nil { - return nil, err - } - stringValue, err := x509_cert.FindOtherNameValue(otherNameValues, x509_cert.PolicyTypeSan, x509_cert.SanTypeOtherName) - uzi, _, _, err := x509_cert.ParseUraFromOtherNameValue(stringValue) - if err != nil { - return nil, err - } - if uzi != serialNumber { - return nil, errors.New("serial number does not match UZI number") - } - template, err := uraCredential(*issuer, signingCert.NotAfter, otherNameValues, subjectTypes, subject) - if err != nil { - return nil, err - } - return vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { - token, err := convertClaims(claims) - if err != nil { - return "", err - } - hdrs, err := convertHeaders(headers) - if err != nil { - return "", err - } - - if hdrs.KeyID() == "" { - err := hdrs.Set("kid", issuer.String()+"#0") - if err != nil { - return "", err - } - } - - // x5c - serializedCert, err := marshalChain(chain...) - if err != nil { - return "", err - } - err = hdrs.Set("x5c", serializedCert) - if err != nil { - return "", err - } - - // x5t - hashSha1 := sha1.Sum(signingCert.Raw) - err = hdrs.Set("x5t", base64.RawURLEncoding.EncodeToString(hashSha1[:])) - if err != nil { - return "", err - } - - sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS256, rsa.PrivateKey(*key), jws.WithProtectedHeaders(hdrs))) - return string(sign), err - }) -} - -// AllowTestUraCa allows the use of Test URA server certificates. -func AllowTestUraCa(allow bool) Option { - return func(o *issueOptions) { - o.allowTestUraCa = allow - } -} - -// IncludePermanentIdentifier includes the permanent identifier in the UZI VC. -func IncludePermanentIdentifier(include bool) Option { - return func(o *issueOptions) { - o.includePermanentIdentifier = include - } -} - -// SubjectAttributes sets the subject attributes to include in the UZI VC. -func SubjectAttributes(attributes ...x509_cert.SubjectTypeName) Option { - return func(o *issueOptions) { - o.subjectAttributes = attributes - } -} - -// 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) { - chainPems := &cert.Chain{} - for _, certificate := range certificates { - err := chainPems.Add([]byte(base64.StdEncoding.EncodeToString(certificate.Raw))) - if err != nil { - return nil, err - } - } - headers, err := x509_cert.FixChainHeaders(chainPems) - return headers, err -} - -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) - if err != nil { - return err - } - } - if x509_cert.IsRootCa(certificate) { - return nil - } - 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)") -} - -// newCertificateChain constructs a valid 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. -// 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 newCertificateChain(certs certificateList) (validCertificateChain, error) { - var signingCert *x509.Certificate - for _, c := range certs { - if c != nil && !c.IsCA { - signingCert = c - break - } - } - if signingCert == nil { - return nil, errors.New("failed to find signing certificate") - } - - var chain []*x509.Certificate - chain = append(chain, signingCert) - - certToCheck := signingCert - for !x509_cert.IsRootCa(certToCheck) { - found := false - for _, c := range certs { - if c == nil || c.Equal(signingCert) { - continue - } - err := certToCheck.CheckSignatureFrom(c) - if err == nil { - chain = append(chain, c) - certToCheck = c - found = true - break - } - } - if !found { - return nil, errors.New("failed to find path from signingCert to root") - } - } - return chain, nil -} - -// convertClaims converts a map of claims to a JWT token. -func convertClaims(claims map[string]interface{}) (jwt.Token, error) { - t := jwt.New() - for k, v := range claims { - if err := t.Set(k, v); err != nil { - return nil, err - } - } - return t, nil -} - -// convertHeaders converts a map of headers to jws.Headers, returning an error if any header fails to set. -func convertHeaders(headers map[string]interface{}) (jws.Headers, error) { - hdr := jws.NewHeaders() - - for k, v := range headers { - if err := hdr.Set(k, v); err != nil { - return nil, err - } - } - return hdr, nil -} - -// uraCredential builds a VerifiableCredential for a given URA and UZI number, including the subject's DID. -func uraCredential(issuerDID did.DID, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID subjectDID) (*vc.VerifiableCredential, error) { - iat := time.Now() - subject := map[string]interface{}{ - "id": subjectDID, - } - var otherNameMap = map[string]string{} - for _, otherNameValue := range otherNameValues { - otherNameMap[string(otherNameValue.Type)] = otherNameValue.Value - } - if len(otherNameMap) > 0 { - subject[string(x509_cert.PolicyTypeSan)] = otherNameMap - } - - var subjectMap = map[string]string{} - for _, subjectType := range subjectTypes { - subjectMap[string(subjectType.Type)] = subjectType.Value - } - if len(subjectMap) > 0 { - subject[string(x509_cert.PolicyTypeSubject)] = subjectMap - } - - id := did.DIDURL{ - DID: issuerDID, - Fragment: uuid.NewString(), - }.URI() - return &vc.VerifiableCredential{ - Issuer: issuerDID.URI(), - Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, - Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI(CredentialType)}, - ID: &id, - IssuanceDate: iat, - ExpirationDate: &expirationDate, - CredentialSubject: []interface{}{subject}, - }, nil -} diff --git a/uzi_vc_issuer/ura_issuer_test.go b/uzi_vc_issuer/ura_issuer_test.go deleted file mode 100644 index 6862a7b..0000000 --- a/uzi_vc_issuer/ura_issuer_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package uzi_vc_issuer - -import ( - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "os" - "testing" - - ssi "github.com/nuts-foundation/go-did" - "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuildUraVerifiableCredential(t *testing.T) { - - chainBytes, err := os.ReadFile("testdata/valid_chain.pem") - require.NoError(t, err, "failed to read chain") - - type inFn = func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) - - defaultIn := func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - pemBlocks, err := parsePemBytes(chainBytes) - require.NoError(t, err, "failed to parse pem blocks") - - certs, err := parseCertificatesFromPemBlocks(pemBlocks) - require.NoError(t, err, "failed to parse certificates from pem blocks") - - privKey, err := NewPrivateKey("testdata/signing_key.pem") - require.NoError(t, err, "failed to read signing key") - - return certs, privKey, "did:example:123" - } - - tests := []struct { - name string - in inFn - errorText string - }{ - { - name: "ok - valid chain", - in: defaultIn, - errorText: "", - }, - // { - // name: "nok - empty chain", - // in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - // _, privKey, didStr := defaultIn(t) - // return []*x509.Certificate{}, privKey, didStr - // }, - // errorText: "empty certificate chain", - // }, - { - name: "nok - empty serial number", - in: func(*testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - certs, privKey, didStr := defaultIn(t) - certs[0].Subject.SerialNumber = "" - return certs, privKey, didStr - }, - errorText: "serialNumber not found in signing certificate", - }, - { - name: "nok - invalid signing serial in signing cert", - in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - certs, privKey, didStr := defaultIn(t) - - certs[0].Subject.SerialNumber = "invalid-serial-number" - return certs, privKey, didStr - }, - errorText: "serial number does not match UZI number", - }, - { - name: "nok - invalid signing certificate 2", - in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - certs, privKey, didStr := defaultIn(t) - - certs[0].ExtraExtensions = make([]pkix.Extension, 0) - certs[0].Extensions = make([]pkix.Extension, 0) - return certs, privKey, didStr - }, - errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", - }, - { - name: "nok - empty cert in chain", - in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - certs, privKey, didStr := defaultIn(t) - certs[0] = &x509.Certificate{} - return certs, privKey, didStr - }, - errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", - }, - // { - // name: "nok - nil signing key", - // in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) { - // certs, _, didStr := defaultIn(t) - // return certs, nil, didStr - // }, - // errorText: "signing key is nil", - // }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - certificates, signingKey, subject := tt.in(t) - _, err := Issue(certificates, signingKey, subjectDID(subject)) - if err != nil { - if err.Error() != tt.errorText { - t.Errorf("BuildUraVerifiableCredential() error = '%v', wantErr '%v'", err.Error(), tt.errorText) - } - } else if err == nil && tt.errorText != "" { - t.Errorf("BuildUraVerifiableCredential() unexpected success, want error") - } - }) - } -} - -func TestNewFileName(t *testing.T) { - // Create a temporary file for testing - tmpFile, err := os.CreateTemp("", "testfile") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tmpFile.Name()) - - tests := []struct { - name string - fileName string - expectErr bool - }{ - {"ValidFile", tmpFile.Name(), false}, - {"InvalidFile", "nonexistentfile", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := newFileName(tt.fileName) - if (err != nil) != tt.expectErr { - t.Errorf("newFileName() error = %v, expectErr %v", err, tt.expectErr) - } - }) - } -} - -func TestReadFile(t *testing.T) { - // Create a temporary file - tmpfile, err := os.CreateTemp("", "example") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpfile.Name()) // clean up - - content := []byte("Hello, World!") - if _, err := tmpfile.Write(content); err != nil { - t.Fatal(err) - } - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - // Call readFile function - fileName := fileName(tmpfile.Name()) - readContent, err := readFile(fileName) - if err != nil { - t.Fatalf("readFile() error = %v", err) - } - - // Assert the content matches - if string(readContent) != string(content) { - t.Errorf("readFile() = %v, want %v", string(readContent), string(content)) - } -} - -func TestIssue(t *testing.T) { - validChain, err := NewValidCertificateChain("testdata/valid_chain.pem") - require.NoError(t, err, "failed to read chain") - - validKey, err := NewPrivateKey("testdata/signing_key.pem") - require.NoError(t, err, "failed to read signing key") - - t.Run("ok - happy path", func(t *testing.T) { - vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization)) - - require.NoError(t, err, "failed to issue verifiable credential") - require.NotNil(t, vc, "verifiable credential is nil") - - assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String()) - assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential"))) - assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential"))) - assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare", vc.Issuer.String()) - - expectedCredentialSubject := []interface{}([]interface{}{map[string]interface{}{ - "id": "did:example:123", - "subject": map[string]interface{}{ - "O": "FauxCare", - }, - "san": map[string]interface{}{ - "otherName": "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333", - "permanentIdentifier.assigner": "2.16.528.1.1007.3.3", - "permanentIdentifier.value": "2222222", - }, - }}) - - assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject) - - assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate, "expiration date of VC must match signing certificate") - }) - - t.Run("ok - correct escaping of special characters", func(t *testing.T) { - validChain, err := NewValidCertificateChain("testdata/valid_chain.pem") - require.NoError(t, err, "failed to read chain") - - validChain[0].Subject.Organization = []string{"FauxCare & Co"} - - vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization)) - - assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare%20%26%20Co", vc.Issuer.String()) - }) - -} - -func TestParsePemBytes(t *testing.T) { - chainBytes, err := os.ReadFile("testdata/valid_chain.pem") - require.NoError(t, err, "failed to read chain") - - tests := []struct { - name string - pemBytes []byte - expectNumBlocks int - expectErr bool - }{ - {"ValidChain", chainBytes, 4, false}, - {"InvalidChain", []byte("invalid pem"), 0, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - blocks, err := parsePemBytes(tt.pemBytes) - if (err != nil) != tt.expectErr { - t.Errorf("parsePemBytes() error = %v, expectErr %v", err, tt.expectErr) - } - - if len(blocks) != tt.expectNumBlocks { - t.Errorf("parsePemBytes() = %v, want %v", len(blocks), tt.expectNumBlocks) - } - }) - } -} - -func TestNewCertificateChain(t *testing.T) { - chainBytes, err := os.ReadFile("testdata/valid_chain.pem") - require.NoError(t, err, "failed to read chain") - - pemBlocks, err := parsePemBytes(chainBytes) - require.NoError(t, err, "failed to parse pem blocks") - - certs, err := parseCertificatesFromPemBlocks(pemBlocks) - require.NoError(t, err, "failed to parse certificates from pem blocks") - - tests := []struct { - name string - errorText string - in func(certs []*x509.Certificate) []*x509.Certificate - out func(certs []*x509.Certificate) []*x509.Certificate - }{ - { - name: "ok - valid cert input", - in: func(certs []*x509.Certificate) []*x509.Certificate { - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return certs - }, - errorText: "", - }, - { - name: "ok - it handles out of order certificates", - in: func(certs []*x509.Certificate) []*x509.Certificate { - certs = []*x509.Certificate{certs[2], certs[0], certs[3], certs[1]} - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return certs - }, - errorText: "", - }, - { - name: "nok - missing signing certificate", - in: func(certs []*x509.Certificate) []*x509.Certificate { - certs = certs[1:] - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return nil - }, - errorText: "failed to find signing certificate", - }, - { - name: "nok - missing root CA certificate", - in: func(certs []*x509.Certificate) []*x509.Certificate { - certs = certs[:3] - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return nil - }, - errorText: "failed to find path from signingCert to root", - }, - { - name: "nok - missing first intermediate CA certificate", - in: func(certs []*x509.Certificate) []*x509.Certificate { - certs = []*x509.Certificate{certs[0], certs[2], certs[3]} - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return nil - }, - errorText: "failed to find path from signingCert to root", - }, - { - name: "nok - missing second intermediate CA certificate", - in: func(certs []*x509.Certificate) []*x509.Certificate { - certs = []*x509.Certificate{certs[0], certs[1], certs[3]} - return certs - }, - out: func(certs []*x509.Certificate) []*x509.Certificate { - return nil - }, - errorText: "failed to find path from signingCert to root", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - inputCerts := tt.in(certs) - expectedCerts := tt.out(certs) - resultCerts, err := newCertificateChain(inputCerts) - if err != nil { - if err.Error() != tt.errorText { - t.Errorf("BuildCertificateChain() error = '%v', wantErr '%v'", err.Error(), tt.errorText) - } - } else if err == nil && tt.errorText != "" { - t.Errorf("BuildCertificateChain() unexpected success, want error") - } - if len(resultCerts) != len(expectedCerts) { - t.Errorf("BuildCertificateChain() expected %d certificates, got %d", len(expectedCerts), len(resultCerts)) - return - } - for i := range resultCerts { - if !resultCerts[i].Equal(expectedCerts[i]) { - t.Errorf("BuildCertificateChain() at index %d expected %v, got %v", i, expectedCerts[i], resultCerts[i]) - } - } - }) - } -} diff --git a/x509_cert/x509_cert.go b/x509_cert/x509_cert.go deleted file mode 100644 index 56ddc06..0000000 --- a/x509_cert/x509_cert.go +++ /dev/null @@ -1,88 +0,0 @@ -package x509_cert - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/asn1" - "errors" - "fmt" - "regexp" - "strings" - - "github.com/lestrrat-go/jwx/v2/cert" -) - -// SubjectAlternativeNameType represents the ASN.1 Object Identifier for Subject Alternative Name. -var ( - SubjectAlternativeNameType = asn1.ObjectIdentifier{2, 5, 29, 17} - PermanentIdentifierType = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3} - OtherNameType = asn1.ObjectIdentifier{2, 5, 5, 5} - UraAssigner = asn1.ObjectIdentifier{2, 16, 528, 1, 1007, 3, 3} -) - -// 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(`^[0-9.]+-\d+-(\d+)-S-(\d+)-00\.000-(\d+)$`) - -// 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 ParseCertificates(derChain [][]byte) ([]*x509.Certificate, error) { - if derChain == nil { - return nil, fmt.Errorf("derChain is nil") - } - chain := make([]*x509.Certificate, len(derChain)) - - for i, certBytes := range derChain { - certificate, err := x509.ParseCertificate(certBytes) - if err != nil { - return nil, err - } - chain[i] = certificate - } - - 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 ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) { - if der == nil { - return nil, fmt.Errorf("der is nil") - } - key, err := x509.ParsePKCS8PrivateKey(der) - if err != nil { - 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") - } - return key.(*rsa.PrivateKey), err -} - -// fixChainHeaders replaces newline characters in the certificate chain headers with escaped newline sequences. -// It processes each certificate in the provided chain and returns a new chain with the modified headers or an error if any occurs. -func FixChainHeaders(chain *cert.Chain) (*cert.Chain, error) { - rv := &cert.Chain{} - for i := 0; i < chain.Len(); i++ { - value, _ := chain.Get(i) - der := strings.ReplaceAll(string(value), "\n", "\\n") - err := rv.AddString(der) - if err != nil { - return nil, err - } - } - return rv, nil -} - -func ParseUraFromOtherNameValue(stringValue string) (uzi string, ura string, agb string, err error) { - submatch := RegexOtherNameValue.FindStringSubmatch(stringValue) - if len(submatch) < 4 { - return "", "", "", errors.New("failed to parse URA from OtherNameValue") - } - return submatch[1], submatch[2], submatch[3], nil -} diff --git a/x509_cert/x509_cert_test.go b/x509_cert/x509_cert_test.go deleted file mode 100644 index 7893b41..0000000 --- a/x509_cert/x509_cert_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package x509_cert - -import ( - "crypto/x509" - "encoding/pem" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseChain(t *testing.T) { - _, chainPem, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "900030787") - failError(t, err) - derChains := make([][]byte, chainPem.Len()) - for i := 0; i < chainPem.Len(); i++ { - certBlock, ok := chainPem.Get(i) - certBlock = []byte(strings.ReplaceAll(string(certBlock), "\\n", "\n")) - block, _ := pem.Decode(certBlock) - assert.NotNil(t, block) - if ok { - derChains[i] = block.Bytes - } else { - t.Fail() - } - } - - testCases := []struct { - name string - derChain [][]byte - errMsg string - }{ - { - name: "Valid Certificates", - derChain: derChains, - }, - { - name: "Nil ChainPem", - derChain: nil, - errMsg: "derChain is nil", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ParseCertificates(tc.derChain) - if err != nil { - if err.Error() != tc.errMsg { - t.Errorf("got error %v, want %v", err, tc.errMsg) - } - } - }) - } -} - -func TestParsePrivateKey(t *testing.T) { - _, _, _, privateKey, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "900030787") - failError(t, err) - privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) - failError(t, err) - - pkcs1PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey) - testCases := []struct { - name string - der []byte - errMsg string - }{ - { - name: "ValidPrivateKey", - der: privateKeyBytes, - }, - { - name: "InvalidPrivateKey", - der: pkcs1PrivateKey, - errMsg: "x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)", - }, - { - name: "NilDER", - der: nil, - errMsg: "der is nil", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ParsePrivateKey(tc.der) - if err != nil { - if err.Error() != tc.errMsg { - t.Errorf("got error %v, want %v", err, tc.errMsg) - } - } - }) - } -} -func failError(t *testing.T, err error) { - if err != nil { - t.Errorf("an error occured: %v", err.Error()) - t.Fatal(err) - } -} diff --git a/x509_cert/x509_utils.go b/x509_cert/x509_utils.go index 9ab5756..5bb8555 100644 --- a/x509_cert/x509_utils.go +++ b/x509_cert/x509_utils.go @@ -1,21 +1,20 @@ package x509_cert import ( - "bytes" "crypto/x509" - "crypto/x509/pkix" "encoding/asn1" "errors" "fmt" + "github.com/nuts-foundation/go-didx509-toolkit/internal" "slices" ) -type OtherName struct { +type otherName struct { TypeID asn1.ObjectIdentifier Value asn1.RawValue `asn1:"tag:0,explicit"` } -type StingAndOid struct { +type stringAndOid struct { Value string Assigner asn1.ObjectIdentifier } @@ -27,8 +26,6 @@ const ( PolicyTypeSubject PolicyType = "subject" ) -type SanType pkix.AttributeTypeAndValue - type SanTypeName string const ( @@ -168,31 +165,22 @@ func SelectSanTypes(certificate *x509.Certificate, subjectAttributes ...SanTypeN return selectedSubjectTypes, nil } -func FindOtherNameValue(value []*OtherNameValue, policyType PolicyType, sanTypeName SanTypeName) (string, error) { - for _, v := range value { - if v != nil && v.PolicyType == policyType && v.Type == sanTypeName { - return v.Value, nil - } - } - return "", fmt.Errorf("failed to find value for policyType: %s and sanTypeName: %s", policyType, sanTypeName) -} - func findPermanentIdentifiers(cert *x509.Certificate) (string, asn1.ObjectIdentifier, error) { value := "" var assigner asn1.ObjectIdentifier for _, extension := range cert.Extensions { - if extension.Id.Equal(SubjectAlternativeNameType) { + if extension.Id.Equal(internal.SubjectAlternativeNameType) { err := forEachSAN(extension.Value, func(tag int, data []byte) error { if tag != 0 { return nil } - var other OtherName + var other otherName _, err := asn1.UnmarshalWithParams(data, &other, "tag:0") if err != nil { return fmt.Errorf("could not parse requested other SAN: %v", err) } - if other.TypeID.Equal(PermanentIdentifierType) { - var x StingAndOid + if other.TypeID.Equal(internal.PermanentIdentifierType) { + var x stringAndOid _, err = asn1.Unmarshal(other.Value.Bytes, &x) if err != nil { return err @@ -216,17 +204,17 @@ func findPermanentIdentifiers(cert *x509.Certificate) (string, asn1.ObjectIdenti func findOtherNameValue(cert *x509.Certificate) (string, error) { value := "" for _, extension := range cert.Extensions { - if extension.Id.Equal(SubjectAlternativeNameType) { + if extension.Id.Equal(internal.SubjectAlternativeNameType) { err := forEachSAN(extension.Value, func(tag int, data []byte) error { if tag != 0 { return nil } - var other OtherName + var other otherName _, err := asn1.UnmarshalWithParams(data, &other, "tag:0") if err != nil { return fmt.Errorf("could not parse requested other SAN: %v", err) } - if other.TypeID.Equal(OtherNameType) { + if other.TypeID.Equal(internal.OtherNameType) { _, err = asn1.Unmarshal(other.Value.Bytes, &value) if err != nil { return err @@ -286,7 +274,3 @@ func processSANSequence(rest []byte, callback func(tag int, data []byte) error) } return nil } - -func IsRootCa(signingCert *x509.Certificate) bool { - return signingCert.IsCA && bytes.Equal(signingCert.RawIssuer, signingCert.RawSubject) -} diff --git a/x509_cert/x509_utils_test.go b/x509_cert/x509_utils_test.go index b3a9df8..cf48702 100644 --- a/x509_cert/x509_utils_test.go +++ b/x509_cert/x509_utils_test.go @@ -2,12 +2,16 @@ package x509_cert import ( "crypto/x509" + "encoding/asn1" + "github.com/nuts-foundation/go-didx509-toolkit/internal" "reflect" "testing" ) +var permanentIdentifierAssigner = asn1.ObjectIdentifier{2, 16, 528, 1, 1007, 3, 3} + func TestFindOtherName(t *testing.T) { - chain, _, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380") + chain, _, _, _, _, err := internal.BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380") if err != nil { t.Fatal(err) } @@ -34,7 +38,7 @@ func TestFindOtherName(t *testing.T) { { PolicyType: PolicyTypeSan, Type: SanTypePermanentIdentifierAssigner, - Value: UraAssigner.String(), + Value: permanentIdentifierAssigner.String(), }, }, wantErr: false,