Skip to content

Commit

Permalink
Initialize project
Browse files Browse the repository at this point in the history
  • Loading branch information
project0 committed Dec 10, 2017
0 parents commit 047a359
Show file tree
Hide file tree
Showing 14 changed files with 1,036 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.idea/

# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM golang:1.9 as builder

WORKDIR /go/src/github.com/project0/certjunkie/

COPY . .
RUN go get -v

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o certjunkie .

FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=builder /go/src/github.com/project0/certjunkie/certjunkie .

ENTRYPOINT ["./certjunkie"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Richard Hillmann

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CertJunkie
This project is inspired by [acme-dns](https://github.com/joohoi/acme-dns). While acme-dns is awesome to use with other acme clients, it lacks of capabilities of shared certs and anonymous usage.

I want to have a simple http server to create, challenge and receive my (lets encrypt) certs from an central point.
As it is intended to be used within an private and closed context, optional authentication and secured connection is currently not focused (fee free to create PR).

## Usage

```
--dns.domain string The NS domain name of this server (default "ns.local")
--dns.listen string Bind on this port to run the DNS server on (tcp and udp) (default ":53")
--dns.zone string The zone we are using to provide the txt records for challenge (default "acme.local")
--email string Registration email for the ACME server
--listen string Bind on this port to run the API server on (default ":80")
--provider string DNS challenge provider name (default "dnscname")
--server string ACME Directory Resource URI (default "https://acme-v01.api.letsencrypt.org/directory")
--storage string Storage driver to use, currently only local is supported (default "local")
--storage.local string Path to store the certs and account data for local storage driver (default "$HOME/.certjunkie")
```

For combatible dns provdider look at https://github.com/xenolf/lego/tree/master/providers/dns

### Docker
[Image DockerHub](https://hub.docker.com/project0de/certjunkie)

```bash
docker run -ti -p 80:80 -p 53:53 -p 53:53/udp \
-v $(pwd)/certjunkie:/storage project0de/certjunkie \
--storage.local /storage --email [email protected] --dns.zone certjunkie.domain.com --dns.domain thisserver.domain.com
```

### Client example with curl
```bash
curl http://localhost:8080/cert/my.domain.de/cert -Fo my.domain.de.crt && \
curl http://localhost:8080/cert/my.domain.de/key -Fo my.domain.de.key && \
curl http://localhost:8080/cert/my.domain.de/ca -Fo my.domain.de.ca
```

## `dnscname` DNS redirect with CNAME
This is actually `$challengeDomain.$dnsDomain.`.
Ensure the NS record is set to this server

### Example
Asume starting with `certjunkie --dns.domain certjunkiens.example.com --dns.zone certjunkie.example.com --email [email protected]`

1. Delegate a subdomain to the server running certbot on your remote hosted DNS `example.com`:
```
certjunkiens A 1.1.1.1 300 # this should be A/AAAA record
certunkie NS certjunkiens.example.com # delegate zone to our built in nameserver
```

2. Setup certjunkie to start with his new authorative domain `certjunkie.example.com`

3. Forward the acme txt record for domains you would like to automate challenge:
```
_acme-challenge.yourdomain.com CNAME yourdomain.com.certjunkie.example.com
_acme-challenge.www.yourdomain.com CNAME www.yourdomain.com.certjunkie.example.com
_acme-challenge.service.cloud.yourdomain.com CNAME service.cloud.yourdomain.com.certjunkie.example.com
```

## API
* `domain`: Get an cert which matches this domain.

### GET /cert/{domain}

Get JSON of an cert with CA and key
If the cert does not exist (or is not valid anymore) it will request a new one (sync).

#### Optional query parameters
* `san`: Comma separated list of subject alternative names the cert must have.
* `onlycn`: Get only a cert which matches the CommonName
* `valid`: How long needs the cert to be valid in days before requesting a new one. Defaults to 30

### GET /cert/{domain}/cert
Retrieve only the certificate pem encoded.

### GET /cert/{domain}/ca
Retrieve only the Issuer Certificate (CA) pem encoded.

### GET /cert/{domain}/bundle
Retrieve bundled cert with ca pem encoded.

### GET /cert/{domain}/key
Retrieve the private key pem encoded.
34 changes: 34 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package api

import (
"log"
"net/http"
"os"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"

"github.com/project0/certjunkie/certstore"
)

func NewApiServer(listen string, store *certstore.CertStore) {

apiCert := apiCert{
store: store,
}

r := mux.NewRouter()
r.HandleFunc("/cert/{domain}", apiCert.getJson).Methods(http.MethodGet)
r.HandleFunc("/cert/{domain}/cert", apiCert.getCert).Methods(http.MethodGet)
r.HandleFunc("/cert/{domain}/ca", apiCert.getCA).Methods(http.MethodGet)
r.HandleFunc("/cert/{domain}/key", apiCert.getKey).Methods(http.MethodGet)
r.HandleFunc("/cert/{domain}/bundle", apiCert.getBundle).Methods(http.MethodGet)

log.Printf("Start listening http server on %s", listen)
go func() {
err := http.ListenAndServe(listen, handlers.LoggingHandler(os.Stdout, r))
if err != nil {
log.Fatalf("Failed to setup the http server: %s\n", err.Error())
}
}()
}
102 changes: 102 additions & 0 deletions api/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package api

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/gorilla/mux"

"github.com/project0/certjunkie/certstore"
)

type apiCert struct {
store *certstore.CertStore
}

// certRequest obtains a cert from the certstore
func (a *apiCert) certRequest(w http.ResponseWriter, r *http.Request) *certstore.CertificateResource {
var err error
vars := mux.Vars(r)
if vars["domain"] == "" {
http.Error(w, fmt.Sprintf("Domain name %s is not valid", vars["domain"]), http.StatusBadRequest)
return nil
}
query := r.URL.Query()
cr := certstore.CertRequest{
Domain: vars["domain"],
ValidDays: 30,
}

if query.Get("onlycn") != "" {
cr.DomainIsCn = true
}

if query.Get("valid") != "" {
cr.ValidDays, err = strconv.Atoi(query.Get("valid"))
if err != nil {
http.Error(w, fmt.Sprintf("Invalid value for parameter valid: %v", err), http.StatusBadRequest)
return nil
}
}

if query.Get("san") != "" {
cr.San = strings.Split(query.Get("san"), ",")
}

cert, err := a.store.GetCertificate(&cr)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}

return cert
}

func (a *apiCert) getJson(w http.ResponseWriter, r *http.Request) {
cert := a.certRequest(w, r)
if cert == nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(cert)
}

func (a *apiCert) getCert(w http.ResponseWriter, r *http.Request) {
cert := a.certRequest(w, r)
if cert == nil {
return
}
w.WriteHeader(http.StatusOK)
w.Write(cert.Certificate)
}

func (a *apiCert) getCA(w http.ResponseWriter, r *http.Request) {
cert := a.certRequest(w, r)
if cert == nil {
return
}
w.WriteHeader(http.StatusOK)
w.Write(cert.IssuerCertificate)
}

func (a *apiCert) getKey(w http.ResponseWriter, r *http.Request) {
cert := a.certRequest(w, r)
if cert == nil {
return
}
w.WriteHeader(http.StatusOK)
w.Write(cert.PrivateKey)
}

func (a *apiCert) getBundle(w http.ResponseWriter, r *http.Request) {
cert := a.certRequest(w, r)
if cert == nil {
return
}
w.WriteHeader(http.StatusOK)
w.Write(append(cert.Certificate, cert.IssuerCertificate...))
}
19 changes: 19 additions & 0 deletions certstore/certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package certstore

import (
"crypto/x509"
"encoding/pem"
)

// CertificateResource represent everything from our cert
type CertificateResource struct {
Domain string `json:"domain"`
PrivateKey []byte `json:"key"`
Certificate []byte `json:"certificate"`
IssuerCertificate []byte `json:"issuer"`
}

func (c *CertificateResource) parseCert() (*x509.Certificate, error) {
block, _ := pem.Decode(c.Certificate)
return x509.ParseCertificate(block.Bytes)
}
Loading

0 comments on commit 047a359

Please sign in to comment.