Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions cas/adcscas/adcscas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package adcscas

import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"math"
"os"
"strings"
"time"

"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wcce"
"github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
"github.com/oiweiwei/go-msrpc/msrpc/icpr/icertpassage/v0"
"github.com/oiweiwei/go-msrpc/ssp"
"github.com/oiweiwei/go-msrpc/ssp/credential"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/oiweiwei/go-msrpc/text/encoding/utf16le"
"github.com/pkg/errors"
"github.com/smallstep/certificates/cas/apiv1"
)

func init() {
apiv1.Register(apiv1.ADCSCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
return New(ctx, opts)
})
}

var now = time.Now

type ADCSOptions struct {
ServerAddr string `json:"serverAddr"`
AuthUser string `json:"authUser"`
AuthPass string `json:"authPass"`

RootCAPath string `json:"rootCAPath"`
IntermediateCAPath string `json:"intermediateCAPath"`

CAName string `json:"caName"`
DefaultTemplateName string `json:"defaultTemplateName"`
TemplateMap map[string]string `json:"templateMap"`
}

// ADCSCAS implements a Certificate Authority Service using Active Directory Certificate Services
type ADCSCAS struct {
Config ADCSOptions
rpcOptions []dcerpc.Option
securityContext context.Context
rootCertificate *x509.Certificate
intermediateCertificates []*x509.Certificate
pkiTargetName string
}

// New creates a new CertificateAuthorityService implementation using Active Directory Certificate Services
func New(ctx context.Context, opts apiv1.Options) (*ADCSCAS, error) {
var adcsConfig ADCSOptions

err := json.Unmarshal(opts.Config, &adcsConfig)
if err != nil {
return nil, fmt.Errorf("error decoding adcsCAS config: %w", err)
}

targetName := strings.ToUpper(strings.SplitN(adcsConfig.ServerAddr, ".", 1)[0])

securityContext := gssapi.NewSecurityContext(ctx)

cred := credential.NewFromPassword(adcsConfig.AuthUser, adcsConfig.AuthPass)

rpcOptions := []dcerpc.Option{
dcerpc.WithMechanism(ssp.NTLM),
dcerpc.WithCredentials(cred),
epm.EndpointMapper(
securityContext,
adcsConfig.ServerAddr,
),
}

var rootCACertificate *x509.Certificate
var intermediateCABundle []*x509.Certificate

if adcsConfig.RootCAPath != "" {
rootPEM, err := os.ReadFile(adcsConfig.RootCAPath)
if err != nil {
return nil, err
}

rootBytes, _ := pem.Decode(rootPEM)
rootCACertificate, err = x509.ParseCertificate(rootBytes.Bytes)
if err != nil {
return nil, err
}
} else {
return nil, errors.New("ADCS CAS requires rootCAPath to be set")
}

if adcsConfig.IntermediateCAPath != "" {
pemFile, err := os.ReadFile(adcsConfig.IntermediateCAPath)
if err != nil {
return nil, err
}

for pemBlock, remainingPEM := pem.Decode(pemFile); pemBlock != nil; pemBlock, remainingPEM = pem.Decode(remainingPEM) {
if pemBlock.Type == "CERTIFICATE" {
intermediateCA, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("error parsing intermediate certificate: %w", err)
}

intermediateCABundle = append(intermediateCABundle, intermediateCA)
}
}
} else {
return nil, errors.New("ADCS CAS requires intermediateCAPath to be set")
}

return &ADCSCAS{
Config: adcsConfig,
rpcOptions: rpcOptions,
rootCertificate: rootCACertificate,
intermediateCertificates: intermediateCABundle,
securityContext: securityContext,
pkiTargetName: targetName,
}, nil
}

// Type returns the type of this CertificateAuthorityService.
func (c *ADCSCAS) Type() apiv1.Type {
return apiv1.ADCSCAS
}

// CreateCertificate signs a new certificate using ADCS.
func (c *ADCSCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
switch {
case req.Template == nil:
return nil, errors.New("createCertificateRequest `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
}

t := now()

// Provisioners can also set specific values.
if req.Template.NotBefore.IsZero() {
req.Template.NotBefore = t.Add(-1 * req.Backdate)
}
if req.Template.NotAfter.IsZero() {
req.Template.NotAfter = t.Add(req.Lifetime)
}

// find a suitable template name
var certificateTemplate = c.Config.DefaultTemplateName
if c.Config.TemplateMap != nil {
certificateTemplate, _ = c.Config.TemplateMap[req.Provisioner.Name]
}

icc, err := dcerpc.Dial(c.securityContext, c.Config.ServerAddr, c.rpcOptions...)
if err != nil {
return nil, fmt.Errorf("error dialing %v: %w", c.Config.ServerAddr, err)
}

defer icc.Close(c.securityContext)

icpClient, err := icertpassage.NewCertPassageClient(
c.securityContext,
icc,
dcerpc.WithTargetName(fmt.Sprintf("host/%s", c.pkiTargetName)),
dcerpc.WithSeal(),
dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy),
)
if err != nil {
return nil, err
}

encodedAttribs, err := utf16le.Encode(fmt.Sprintf("CertificateTemplate:%s\n", certificateTemplate) + string(rune(0)))
if err != nil {
return nil, err
}

if len(encodedAttribs) >= math.MaxUint32 {
return nil, errors.New("certificateTemplate contains too many attribs")
}

if len(req.CSR.Raw) >= math.MaxUint32 {
return nil, errors.New("csr too long for request")
}

icpReq := icertpassage.CertServerRequestRequest{
Flags: 0,
Authority: c.Config.CAName,
RequestID: 0,
Attributes: &wcce.CertTransportBlob{
Length: uint32(len(encodedAttribs)), //nolint:gosec // disable G115
Buffer: encodedAttribs,
},
Request: &wcce.CertTransportBlob{
Length: uint32(len(req.CSR.Raw)), //nolint:gosec // disable G115
Buffer: req.CSR.Raw,
},
}

certResponse, err := icpClient.CertServerRequest(c.securityContext, &icpReq)

if err != nil {
return nil, err
}

switch certResponse.Disposition {
case 3:
issuedCert, err := x509.ParseCertificate(certResponse.EncodedCert.Buffer)
if err != nil {
return nil, fmt.Errorf("error parsing returned certificate: %w", err)
}
return &apiv1.CreateCertificateResponse{
Certificate: issuedCert,
CertificateChain: c.intermediateCertificates,
}, nil
case 5:
return nil, errors.New("CertServerRequest Pending Approval")
default:
msg, err := utf16le.Decode(certResponse.DispositionMessage.Buffer)
if err != nil {
return nil, errors.New("Error decoding error message from ADCS: " + err.Error())
}
return nil, errors.New(msg)
}
}

// RenewCertificate signs the given certificate template. In ADCSCAS this is not implemented.
func (c *ADCSCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
return nil, apiv1.NotImplementedError{Message: "adcsCAS does not support renewals"}
}

// RevokeCertificate revokes the given certificate. In ADCSCAS this
// is not implemented, but it is possible to send a revocation request using the icertadmind service:
//
// https://github.com/oiweiwei/go-msrpc/blob/60ff6238355b5e7d1ff8f86a3d80ec7b0b523fb3/msrpc/dcom/csra/icertadmind/v0/v0.go#L83
func (c *ADCSCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
return nil, apiv1.NotImplementedError{Message: "adcsCAS does not support revocation"}
}

func (c *ADCSCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
return &apiv1.GetCertificateAuthorityResponse{
RootCertificate: c.rootCertificate,
IntermediateCertificates: c.intermediateCertificates,
}, nil
}
2 changes: 2 additions & 0 deletions cas/apiv1/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const (
VaultCAS = "vaultcas"
// ExternalCAS is a CertificateAuthorityService using an external injected CA implementation
ExternalCAS = "externalcas"
// ADCSCAS is a CertificateAuthorityService using Active Directory Certificate Services
ADCSCAS = "adcscas"
)

// String returns a string from the type. It will always return the lower case
Expand Down
1 change: 1 addition & 0 deletions cmd/step-ca/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
_ "go.step.sm/crypto/kms/yubikey"

// Enabled cas interfaces.
_ "github.com/smallstep/certificates/cas/adcscas"
_ "github.com/smallstep/certificates/cas/cloudcas"
_ "github.com/smallstep/certificates/cas/softcas"
_ "github.com/smallstep/certificates/cas/stepcas"
Expand Down
16 changes: 14 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/smallstep/certificates

go 1.24.0
go 1.24.1

require (
cloud.google.com/go/longrunning v0.7.0
Expand All @@ -23,6 +23,7 @@ require (
github.com/hashicorp/vault/api/auth/aws v0.11.0
github.com/hashicorp/vault/api/auth/kubernetes v0.10.0
github.com/newrelic/go-agent/v3 v3.41.0
github.com/oiweiwei/go-msrpc v1.2.12
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/rs/xid v1.6.0
Expand Down Expand Up @@ -93,6 +94,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand All @@ -118,13 +120,20 @@ require (
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/indece-official/go-ebcdic v1.2.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
Expand All @@ -138,12 +147,15 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 // indirect
github.com/peterbourgon/diskv/v3 v3.0.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rs/zerolog v1.32.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/schollz/jsonstore v1.1.0 // indirect
Expand Down
Loading