diff --git a/x509/signing/signer.go b/x509/signing/signer.go new file mode 100644 index 0000000..20f8676 --- /dev/null +++ b/x509/signing/signer.go @@ -0,0 +1,65 @@ +package signing + +import ( + "crypto/x509" + "crypto/x509/pkix" + "time" +) + +type SignerRequest struct { + csr *x509.CertificateRequest + subjectDN pkix.Name + emails []string + dnsNames []string + duration time.Duration + signatureAlgorithm x509.SignatureAlgorithm +} + +func (s *SignerRequest) SignatureAlgorithm() x509.SignatureAlgorithm { + return s.signatureAlgorithm +} + +func (s *SignerRequest) Duration() time.Duration { + return s.duration +} + +func (s *SignerRequest) DnsNames() []string { + return s.dnsNames +} + +func (s *SignerRequest) Emails() []string { + return s.emails +} + +func (s *SignerRequest) Csr() *x509.CertificateRequest { + return s.csr +} + +func (s *SignerRequest) SubjectDN() pkix.Name { + return s.subjectDN +} + +func NewSignerRequest( + csr *x509.CertificateRequest, + subjectDN pkix.Name, + emails, dnsNames []string, + duration time.Duration, + signatureAlgorithm x509.SignatureAlgorithm, +) *SignerRequest { + return &SignerRequest{ + csr: csr, + subjectDN: subjectDN, + emails: emails, + dnsNames: dnsNames, + duration: duration, + signatureAlgorithm: signatureAlgorithm, + } +} + +type SignerResponse interface { + Certificate() *x509.Certificate +} + +type Signer interface { + SignCertificate(*SignerRequest) (SignerResponse, error) +} diff --git a/x509/signing/signing.go b/x509/signing/signing.go index 447623a..0b08883 100644 --- a/x509/signing/signing.go +++ b/x509/signing/signing.go @@ -1,11 +1,87 @@ package signing -type X509Signing struct{} +import ( + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "time" +) -type RequestInformation struct{} +type X509Signing struct { + signer Signer + repo Repository +} + +func NewX509Signing(signer Signer, repo Repository) *X509Signing { + return &X509Signing{signer: signer, repo: repo} +} + +type CertificatePolicyId int + +type RequestSignature struct { + rawCSRData []byte + subjectCommonName string + emails []string + dnsNames []string + duration time.Duration + signatureAlgorithm x509.SignatureAlgorithm +} + +func NewRequestSignature( + csrBytes []byte, + cn string, + emails, dnsNames []string, + duration time.Duration, + signatureAlgorithm x509.SignatureAlgorithm, +) *RequestSignature { + return &RequestSignature{ + rawCSRData: csrBytes, + subjectCommonName: cn, + emails: emails, + dnsNames: dnsNames, + duration: duration, + signatureAlgorithm: signatureAlgorithm, + } +} -type CertificateSigned struct{} +type CertificateSigned struct { + certificate *x509.Certificate +} + +func (c CertificateSigned) Certificate() *x509.Certificate { + return c.certificate +} + +func (x *X509Signing) Sign(signingRequest *RequestSignature) (*CertificateSigned, error) { + // validate request content + csr, err := x509.ParseCertificateRequest(signingRequest.rawCSRData) + if err != nil { + return nil, fmt.Errorf("could not parse CSR data: %w", err) + } + + certificateFromSigner, err := x.signer.SignCertificate( + NewSignerRequest( + csr, + pkix.Name{CommonName: signingRequest.subjectCommonName}, + signingRequest.emails, + signingRequest.dnsNames, + signingRequest.duration, + signingRequest.signatureAlgorithm, + ), + ) + if err != nil { + return nil, err + } + + result := NewCertificateSigned(certificateFromSigner) + err = x.repo.StoreCertificate(result) + if err != nil { + return nil, err + } + + return result, nil +} -func (x *X509Signing) Sign(signingRequest *RequestInformation) (*CertificateSigned, error) { - return &CertificateSigned{}, nil +func NewCertificateSigned(signed SignerResponse) *CertificateSigned { + return &CertificateSigned{certificate: signed.Certificate()} } diff --git a/x509/signing/signing_test.go b/x509/signing/signing_test.go new file mode 100644 index 0000000..3a9fee4 --- /dev/null +++ b/x509/signing/signing_test.go @@ -0,0 +1,118 @@ +package signing_test + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + rand2 "math/rand" + "testing" + "time" + + "git.cacert.org/cacert-gosigner/x509/signing" + "github.com/stretchr/testify/assert" +) + +type testRepo struct { + certs map[string]x509.Certificate +} + +func (r *testRepo) StoreCertificate(c *signing.CertificateSigned) error { + cert := c.Certificate() + r.certs[cert.SerialNumber.Text(16)] = *cert + return nil +} + +type testSigner struct { + key crypto.PrivateKey + certificate *x509.Certificate +} + +type testSignerResponse struct { + certificate *x509.Certificate +} + +func (t testSignerResponse) Certificate() *x509.Certificate { + return t.certificate +} + +func newTestSignerResponse(certificate *x509.Certificate) *testSignerResponse { + return &testSignerResponse{certificate: certificate} +} + +func (s *testSigner) SignCertificate(request *signing.SignerRequest) (signing.SignerResponse, error) { + startDate := time.Now().Add(-1 * time.Minute) + template := &x509.Certificate{ + Subject: request.SubjectDN(), + SerialNumber: big.NewInt(rand2.Int63()), + EmailAddresses: request.Emails(), + NotBefore: startDate, + NotAfter: startDate.Add(request.Duration()), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection}, + SignatureAlgorithm: request.SignatureAlgorithm(), + } + + certBytes, err := x509.CreateCertificate(rand.Reader, template, s.certificate, request.Csr().PublicKey, s.key) + if err != nil { + return nil, err + } + + certificate, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, err + } + + return newTestSignerResponse(certificate), nil +} + +func TestSigning(t *testing.T) { + rand2.Seed(time.Now().UnixMilli()) + + testRepository := testRepo{certs: make(map[string]x509.Certificate)} + testSigner := newTestSigner(t) + s := signing.NewX509Signing(testSigner, &testRepository) + + csrKey, err := rsa.GenerateKey(rand.Reader, 3072) + if err != nil { + t.Errorf("could not generate key pair") + } + + csrTemplate := &x509.CertificateRequest{PublicKey: csrKey.Public()} + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, csrKey) + if err != nil { + t.Error(err) + } + + testRequest := signing.NewRequestSignature(csrBytes, "Test Subject", []string{"test@example.org"}, nil, 365*24*time.Hour, x509.SHA384WithRSA) + signed, err := s.Sign(testRequest) + if err != nil { + t.Error(err) + } + + cert := signed.Certificate() + assert.Contains(t, testRepository.certs, cert.SerialNumber.Text(16)) + assert.Equal(t, cert.Subject.CommonName, "Test Subject") + assert.Contains(t, cert.EmailAddresses, "test@example.org") +} + +func newTestSigner(t *testing.T) *testSigner { + t.Helper() + caKey, err := rsa.GenerateKey(rand.Reader, 3072) + if err != nil { + t.Fatalf("could not generate key pair: %v", err) + } + caTemplate := &x509.Certificate{Subject: pkix.Name{CommonName: "Test CA"}, SerialNumber: big.NewInt(rand2.Int63())} + + certificateBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatalf("could not self-sign CA certificate: %v", err) + } + caCertificate, err := x509.ParseCertificate(certificateBytes) + if err != nil { + t.Fatalf("could not create test CA certificate: %v", err) + } + return &testSigner{key: caKey, certificate: caCertificate} +}