Merge branch 'master' into herman/acme-da-roots

pull/1040/head
Herman Slatman 2 years ago
commit ce3215c702
No known key found for this signature in database
GPG Key ID: F4D8A44EA0A75A4F

@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
---
## [Unreleased]
### Added
- Added support for ACME device-attest-01 challenge.
## [0.22.1] - 2022-08-31
### Fixed

@ -35,7 +35,7 @@ To get up and running quickly, or as an alternative to running your own `step-ca
[![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates)
[![Build Status](https://travis-ci.com/smallstep/certificates.svg?branch=master)](https://travis-ci.com/smallstep/certificates)
[![Build Status](https://github.com/smallstep/certificates/actions/workflows/test.yml/badge.svg)](https://github.com/smallstep/certificates)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![CLA assistant](https://cla-assistant.io/readme/badge/smallstep/certificates)](https://cla-assistant.io/smallstep/certificates)

@ -3,6 +3,7 @@ package api
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"fmt"
"io"
@ -49,6 +50,10 @@ func (*fakeProvisioner) IsAttestationFormatEnabled(ctx context.Context, format p
return true
}
func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
return nil, false
}
func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil }
func (*fakeProvisioner) GetID() string { return "" }
func (*fakeProvisioner) GetName() string { return "" }

@ -354,7 +354,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
switch att.Format {
case "apple":
data, err := doAppleAttestationFormat(ctx, ch, db, &att)
data, err := doAppleAttestationFormat(ctx, prov, ch, &att)
if err != nil {
var acmeError *Error
if errors.As(err, &acmeError) {
@ -382,7 +382,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
}
case "step":
data, err := doStepAttestationFormat(ctx, ch, jwk, &att)
data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
if err != nil {
var acmeError *Error
if errors.As(err, &acmeError) {
@ -593,13 +593,17 @@ type appleAttestationData struct {
Certificate *x509.Certificate
}
func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*appleAttestationData, error) {
root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing apple enterprise ca")
func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) {
// Use configured or default attestation roots if none is configured.
roots, ok := prov.GetAttestationRoots()
if !ok {
root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing apple enterprise ca")
}
roots = x509.NewCertPool()
roots.AddCert(root)
}
roots := x509.NewCertPool()
roots.AddCert(root)
x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok {
@ -690,13 +694,17 @@ type stepAttestationData struct {
SerialNumber string
}
func doStepAttestationFormat(ctx context.Context, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing root ca")
func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
// Use configured or default attestation roots if none is configured.
roots, ok := prov.GetAttestationRoots()
if !ok {
root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing root ca")
}
roots = x509.NewCertPool()
roots.AddCert(root)
}
roots := x509.NewCertPool()
roots.AddCert(root)
// Extract x5c and verify certificate
x5c, ok := att.AttStatement["x5c"].([]interface{})

@ -4,6 +4,8 @@ import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
@ -13,6 +15,7 @@ import (
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
@ -20,13 +23,18 @@ import (
"net"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"go.step.sm/crypto/jose"
"github.com/fxamacker/cbor/v2"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
)
type mockClient struct {
@ -37,8 +45,25 @@ type mockClient struct {
func (m *mockClient) Get(url string) (*http.Response, error) { return m.get(url) }
func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) }
func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {
return m.tlsDial(network, addr, config)
func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls.Conn, error) {
return m.tlsDial(network, addr, tlsConfig)
}
func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner {
t.Helper()
prov := &provisioner.ACME{
Type: "ACME",
Name: "acme",
Challenges: []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01},
AttestationRoots: roots,
}
if err := prov.Init(provisioner.Config{
Claims: config.GlobalProvisionerClaims,
}); err != nil {
t.Fatal(err)
}
return prov
}
func Test_storeError(t *testing.T) {
@ -2400,3 +2425,352 @@ func Test_http01ChallengeHost(t *testing.T) {
})
}
}
func Test_doAppleAttestationFormat(t *testing.T) {
ctx := context.Background()
ca, err := minica.New()
if err != nil {
t.Fatal(err)
}
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
{Id: oidAppleNonce, Value: []byte("nonce")},
},
})
if err != nil {
t.Fatal(err)
}
type args struct {
ctx context.Context
prov Provisioner
ch *Challenge
att *AttestationObject
}
tests := []struct {
name string
args args
want *appleAttestationData
wantErr bool
}{
{"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
}}, &appleAttestationData{
Nonce: []byte("nonce"),
SerialNumber: "serial-number",
UDID: "udid",
SEPVersion: "16.0",
Certificate: leaf,
}, false},
{"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
}}, nil, true},
{"fail missing x5c", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"foo": "bar",
},
}}, nil, true},
{"fail empty issuer", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{},
},
}}, nil, true},
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
},
}}, nil, true},
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
},
}}, nil, true},
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, "intermediate"},
},
}}, nil, true},
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
},
}}, nil, true},
{"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw},
},
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := doAppleAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.att)
if (err != nil) != tt.wantErr {
t.Errorf("doAppleAttestationFormat() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("doAppleAttestationFormat() = %v, want %v", got, tt.want)
}
})
}
}
func Test_doStepAttestationFormat(t *testing.T) {
ctx := context.Background()
ca, err := minica.New()
if err != nil {
t.Fatal(err)
}
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
mustSigner := func(kty, crv string, size int) crypto.Signer {
s, err := keyutil.GenerateSigner(kty, crv, size)
if err != nil {
t.Fatal(err)
}
return s
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
serialNumber, err := asn1.Marshal(1234)
if err != nil {
t.Fatal(err)
}
leaf := makeLeaf(signer, serialNumber)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
if err != nil {
t.Fatal(err)
}
keyAuth, err := KeyAuthorization("token", jwk)
if err != nil {
t.Fatal(err)
}
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
if err != nil {
t.Fatal(err)
}
cborSig, err := cbor.Marshal(sig)
if err != nil {
t.Fatal(err)
}
otherSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
otherSig, err := otherSigner.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
if err != nil {
t.Fatal(err)
}
otherCBORSig, err := cbor.Marshal(otherSig)
if err != nil {
t.Fatal(err)
}
type args struct {
ctx context.Context
prov Provisioner
ch *Challenge
jwk *jose.JSONWebKey
att *AttestationObject
}
tests := []struct {
name string
args args
want *stepAttestationData
wantErr bool
}{
{"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, &stepAttestationData{
SerialNumber: "1234",
Certificate: leaf,
}, false},
{"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": [][]byte{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, "intermediate"},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": string(cborSig),
},
}}, nil, true},
{"fail sig unmarshal", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": []byte("bad-sig"),
},
}}, nil, true},
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify P-256", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": otherCBORSig,
},
}}, nil, true},
{"fail sig verify P-384", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("EC", "P-384", 0), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify RSA", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("RSA", "", 2048), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail sig verify Ed25519", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(mustSigner("OKP", "Ed25519", 0), serialNumber).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
{"fail unmarshal serial number", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{makeLeaf(signer, []byte("bad-serial")).Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
if (err != nil) != tt.wantErr {
t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want)
}
})
}
}

@ -73,6 +73,7 @@ type Provisioner interface {
AuthorizeRevoke(ctx context.Context, token string) error
IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool
IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
GetAttestationRoots() (*x509.CertPool, bool)
GetID() string
GetName() string
DefaultTLSCertDuration() time.Duration
@ -113,6 +114,7 @@ type MockProvisioner struct {
MauthorizeRevoke func(ctx context.Context, token string) error
MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool
MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
MgetAttestationRoots func() (*x509.CertPool, bool)
MdefaultTLSCertDuration func() time.Duration
MgetOptions func() *provisioner.Options
}
@ -165,6 +167,13 @@ func (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format
return m.Merr == nil
}
func (m *MockProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
if m.MgetAttestationRoots != nil {
return m.MgetAttestationRoots()
}
return m.Mret1.(*x509.CertPool), m.Mret1 != nil
}
// DefaultTLSCertDuration mock
func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {
if m.MdefaultTLSCertDuration != nil {

@ -77,7 +77,7 @@ func TestDB_getDBAuthz(t *testing.T) {
Token: "token",
CreatedAt: now,
ExpiresAt: now.Add(5 * time.Minute),
Error: acme.NewErrorISE("force"),
Error: acme.NewErrorISE("The server experienced an internal error"),
ChallengeIDs: []string{"foo", "bar"},
Wildcard: true,
}
@ -254,7 +254,7 @@ func TestDB_GetAuthorization(t *testing.T) {
Token: "token",
CreatedAt: now,
ExpiresAt: now.Add(5 * time.Minute),
Error: acme.NewErrorISE("force"),
Error: acme.NewErrorISE("The server experienced an internal error"),
ChallengeIDs: []string{"foo", "bar"},
Wildcard: true,
}
@ -532,7 +532,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error())
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nil, false, errors.New("force")
},
},
@ -582,7 +582,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error())
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nu, true, nil
},
},

@ -72,7 +72,7 @@ func TestDB_getDBChallenge(t *testing.T) {
Value: "test.ca.smallstep.com",
CreatedAt: clock.Now(),
ValidatedAt: "foobar",
Error: acme.NewErrorISE("force"),
Error: acme.NewErrorISE("The server experienced an internal error"),
}
b, err := json.Marshal(dbc)
assert.FatalError(t, err)
@ -264,7 +264,7 @@ func TestDB_GetChallenge(t *testing.T) {
Value: "test.ca.smallstep.com",
CreatedAt: clock.Now(),
ValidatedAt: "foobar",
Error: acme.NewErrorISE("force"),
Error: acme.NewErrorISE("The server experienced an internal error"),
}
b, err := json.Marshal(dbc)
assert.FatalError(t, err)
@ -354,7 +354,7 @@ func TestDB_UpdateChallenge(t *testing.T) {
ID: chID,
Status: acme.StatusValid,
ValidatedAt: "foobar",
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
}
return test{
ch: updCh,
@ -428,7 +428,7 @@ func TestDB_UpdateChallenge(t *testing.T) {
assert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt)
assert.Equals(t, dbNew.Status, acme.StatusValid)
assert.Equals(t, dbNew.ValidatedAt, "foobar")
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error())
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nu, true, nil
},
},

@ -80,7 +80,7 @@ func TestDB_getDBOrder(t *testing.T) {
{Type: "dns", Value: "example.foo.com"},
},
AuthorizationIDs: []string{"foo", "bar"},
Error: acme.NewError(acme.ErrorMalformedType, "force"),
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
}
b, err := json.Marshal(dbo)
assert.FatalError(t, err)
@ -185,7 +185,7 @@ func TestDB_GetOrder(t *testing.T) {
{Type: "dns", Value: "example.foo.com"},
},
AuthorizationIDs: []string{"foo", "bar"},
Error: acme.NewError(acme.ErrorMalformedType, "force"),
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
}
b, err := json.Marshal(dbo)
assert.FatalError(t, err)
@ -284,7 +284,7 @@ func TestDB_UpdateOrder(t *testing.T) {
ID: orderID,
Status: acme.StatusValid,
CertificateID: "certID",
Error: acme.NewError(acme.ErrorMalformedType, "force"),
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
}
return test{
o: o,
@ -324,7 +324,7 @@ func TestDB_UpdateOrder(t *testing.T) {
ID: orderID,
Status: acme.StatusValid,
CertificateID: "certID",
Error: acme.NewError(acme.ErrorMalformedType, "force"),
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
}
return test{
o: o,
@ -372,7 +372,7 @@ func TestDB_UpdateOrder(t *testing.T) {
assert.Equals(t, tc.o.ID, dbo.ID)
assert.Equals(t, tc.o.CertificateID, "certID")
assert.Equals(t, tc.o.Status, acme.StatusValid)
assert.Equals(t, tc.o.Error.Error(), acme.NewError(acme.ErrorMalformedType, "force").Error())
assert.Equals(t, tc.o.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
}
}
})
@ -659,7 +659,7 @@ func TestDB_updateAddOrderIDs(t *testing.T) {
assert.Equals(t, newdbo.ID, "foo")
assert.Equals(t, newdbo.Status, acme.StatusInvalid)
assert.Equals(t, newdbo.ExpiresAt, expiry)
assert.Equals(t, newdbo.Error.Error(), acme.NewError(acme.ErrorMalformedType, "order has expired").Error())
assert.Equals(t, newdbo.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nil, false, errors.New("force")
},
},

@ -335,9 +335,12 @@ func (e *Error) StatusCode() int {
return e.Status
}
// Error allows AError to implement the error interface.
// Error implements the error interface.
func (e *Error) Error() string {
return e.Detail
if e.Err == nil {
return e.Detail
}
return e.Err.Error()
}
// Cause returns the internal error and implements the Causer interface.

@ -157,6 +157,9 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
data := x509util.NewTemplateData()
data.SetCommonName(csr.Subject.CommonName)
// Custom sign options passed to authority.Sign
var extraOptions []provisioner.SignOption
// TODO: support for multiple identifiers?
var permanentIdentifier string
for i := range o.Identifiers {
@ -173,6 +176,9 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
Type: x509util.PermanentIdentifierType,
Value: permanentIdentifier,
})
extraOptions = append(extraOptions, provisioner.AttestationData{
PermanentIdentifier: permanentIdentifier,
})
} else {
defaultTemplate = x509util.DefaultLeafTemplate
sans, err := o.sans(csr)
@ -193,7 +199,11 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
if err != nil {
return WrapErrorISE(err, "error creating template options from ACME provisioner")
}
// Build extra signing options.
signOps = append(signOps, templateOptions)
signOps = append(signOps, extraOptions...)
// Sign a new certificate.
certChain, err := auth.Sign(csr, provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(o.NotBefore),

@ -434,7 +434,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
audiences := a.config.GetAudiences().Renew
if !matchesAudience(claims.Audience, audiences) {
return nil, errs.InternalServerErr(err, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
}
// validate issuer: old versions used the provisioner name, new version uses

@ -96,14 +96,14 @@ type ACME struct {
// provisioner. If this value is not set the default apple, step and tpm
// will be used.
AttestationFormats []ACMEAttestationFormat `json:"attestationFormats,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
// TODO(hs): WIP configuration for ACME Device Attestation
AttestationRoots []byte `json:"attestationRoots"`
// AttestationRoots contains a bundle of root certificates in PEM format
// that will be used to verify the attestation certificates. If provided,
// this bundle will be used even for well-known CAs like Apple and Yubico.
AttestationRoots []byte `json:"attestationRoots,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
attestationRootPool *x509.CertPool
ctl *Controller
ctl *Controller
}
// GetID returns the provisioner unique identifier.
@ -192,6 +192,29 @@ func (p *ACME) Init(config Config) (err error) {
}
}
// Parse attestation roots.
// The pool will be nil if the there are not roots.
if rest := p.AttestationRoots; len(rest) > 0 {
var block *pem.Block
var hasCert bool
p.attestationRootPool = x509.NewCertPool()
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return errors.New("error parsing attestationRoots: malformed certificate")
}
p.attestationRootPool.AddCert(cert)
hasCert = true
}
if !hasCert {
return errors.New("error parsing attestationRoots: no certificates found")
}
}
p.ctl, err = NewController(p, p.Claims, config, p.Options)
return
}
@ -291,12 +314,6 @@ func (p *ACME) IsChallengeEnabled(ctx context.Context, challenge ACMEChallenge)
return false
}
// TODO(hs): we may not want to expose the root pool like this;
// call into an interface function instead to authorize?
func (p *ACME) GetAttestationRoots() (*x509.CertPool, error) {
return p.attestationRootPool, nil
}
// IsAttestationFormatEnabled checks if the given attestation format is enabled.
// By default apple, step and tpm are enabled, to disable any of them the
// AttestationFormat provisioner property should have at least one element.
@ -314,3 +331,9 @@ func (p *ACME) IsAttestationFormatEnabled(ctx context.Context, format ACMEAttest
}
return false
}
// GetAttestationRoots returns certificate pool with the configured attestation
// roots and reports if the pool contains at least one certificate.
func (p *ACME) GetAttestationRoots() (*x509.CertPool, bool) {
return p.attestationRootPool, p.attestationRootPool != nil
}

@ -1,11 +1,13 @@
package provisioner
import (
"bytes"
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
"os"
"testing"
"time"
@ -77,6 +79,15 @@ func TestACME_Getters(t *testing.T) {
}
func TestACME_Init(t *testing.T) {
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
if err != nil {
t.Fatal(err)
}
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
if err != nil {
t.Fatal(err)
}
type ProvisionerValidateTest struct {
p *ACME
err error
@ -120,6 +131,18 @@ func TestACME_Init(t *testing.T) {
err: errors.New("acme attestation format \"zar\" is not supported"),
}
},
"fail-parse-attestation-roots": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("-----BEGIN CERTIFICATE-----\nZm9v\n-----END CERTIFICATE-----")},
err: errors.New("error parsing attestationRoots: malformed certificate"),
}
},
"fail-empty-attestation-roots": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &ACME{Name: "foo", Type: "bar", AttestationRoots: []byte("\n")},
err: errors.New("error parsing attestationRoots: no certificates found"),
}
},
"ok": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &ACME{Name: "foo", Type: "bar"},
@ -132,6 +155,7 @@ func TestACME_Init(t *testing.T) {
Type: "bar",
Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01},
AttestationFormats: []ACMEAttestationFormat{APPLE, STEP},
AttestationRoots: bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n")),
},
}
},
@ -144,6 +168,7 @@ func TestACME_Init(t *testing.T) {
for name, get := range tests {
t.Run(name, func(t *testing.T) {
tc := get(t)
t.Log(string(tc.p.AttestationRoots))
err := tc.p.Init(config)
if err != nil {
if assert.NotNil(t, tc.err) {
@ -346,3 +371,58 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) {
})
}
}
func TestACME_GetAttestationRoots(t *testing.T) {
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
if err != nil {
t.Fatal(err)
}
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
if err != nil {
t.Fatal(err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(appleCA)
pool.AppendCertsFromPEM(yubicoCA)
type fields struct {
Type string
Name string
AttestationRoots []byte
}
tests := []struct {
name string
fields fields
want *x509.CertPool
want1 bool
}{
{"ok", fields{"ACME", "acme", bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n"))}, pool, true},
{"nil", fields{"ACME", "acme", nil}, nil, false},
{"empty", fields{"ACME", "acme", []byte{}}, nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &ACME{
Type: tt.fields.Type,
Name: tt.fields.Name,
AttestationRoots: tt.fields.AttestationRoots,
}
if err := p.Init(Config{
Claims: globalProvisionerClaims,
Audiences: testAudiences,
}); err != nil {
t.Fatal(err)
}
got, got1 := p.GetAttestationRoots()
if tt.want == nil && got != nil {
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
} else if !tt.want.Equal(got) {
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("ACME.GetAttestationRoots() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

@ -77,6 +77,12 @@ func (fn CertificateEnforcerFunc) Enforce(cert *x509.Certificate) error {
return fn(cert)
}
// AttestationData is a SignOption used to pass attestation information to the
// sign methods.
type AttestationData struct {
PermanentIdentifier string
}
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
// SAN provided is the given email address.
type emailOnlyIdentity string

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq/bxpm8frF15hzcwCgYIKoZIzj0EAwMw
UTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB
MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx
MjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug
QXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE
BhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq+Ps9Q4CoT8t8q+UnOe2p
oT9nRaUfGhBTbgvqSGXPjVkbYlIWYO+1zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2/y7
7GEicps9wn2tj+G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB/wQFMAMB
Af8wHQYDVR0OBBYEFPNqTQGd8muBpV5du+UIbVbi+d66MA4GA1UdDwEB/wQEAwIB
BjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5
8m9sX/b2+eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS/u4pTnzoz632rA+xW/T
ZwFEh9bhKjJ+5VQ9/Do1os0u3LEkgN/r
-----END CERTIFICATE-----

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1
YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY
DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg
U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2
cMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E
ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq
joU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH
BVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf
wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet
X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0
1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s
XhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2
lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d
bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq
Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8
SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7
-----END CERTIFICATE-----

@ -94,6 +94,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
var prov provisioner.Interface
var pInfo *casapi.ProvisionerInfo
var attData provisioner.AttestationData
for _, op := range extraOpts {
switch k := op.(type) {
// Capture current provisioner
@ -129,6 +130,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
case provisioner.CertificateEnforcer:
certEnforcers = append(certEnforcers, k)
// Extra information from ACME attestations.
case provisioner.AttestationData:
attData = k
// TODO(mariano,areed): remove me once attData is used.
_ = attData
default:
return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...)
}

@ -14,6 +14,7 @@ import (
"io"
"net"
"os"
"path/filepath"
"reflect"
"testing"
"time"
@ -402,6 +403,14 @@ func TestNew_real(t *testing.T) {
})
}
failDefaultCredentials := true
if home, err := os.UserHomeDir(); err == nil {
file := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json")
if _, err := os.Stat(file); err == nil {
failDefaultCredentials = false
}
}
type args struct {
ctx context.Context
opts apiv1.Options
@ -412,7 +421,7 @@ func TestNew_real(t *testing.T) {
args args
wantErr bool
}{
{"fail default credentials", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, true},
{"fail default credentials", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, failDefaultCredentials},
{"fail certificate authority", false, args{context.Background(), apiv1.Options{}}, true},
{"fail with credentials", false, args{context.Background(), apiv1.Options{
CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/missing.json",

@ -18,6 +18,7 @@ require (
github.com/go-piv/piv-go v1.10.0 // indirect
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.8
github.com/google/go-tpm v0.3.3
github.com/google/uuid v1.3.0
github.com/googleapis/gax-go/v2 v2.4.0
github.com/hashicorp/vault/api v1.3.1
@ -31,6 +32,7 @@ require (
github.com/newrelic/go-agent/v3 v3.18.0
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
github.com/ryboe/q v1.0.17
github.com/sirupsen/logrus v1.8.1
github.com/slackhq/nebula v1.5.2
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
@ -40,7 +42,7 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.4
go.step.sm/crypto v0.19.0
go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb
go.step.sm/linkedca v0.19.0-rc.1
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
@ -51,11 +53,6 @@ require (
gopkg.in/square/go-jose.v2 v2.6.0
)
require (
github.com/google/go-tpm v0.3.3
github.com/ryboe/q v1.0.17
)
require (
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.1.0 // indirect

@ -675,8 +675,8 @@ go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0=
go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw=
go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb h1:qCG7i7PAZcTDLqyFmOzBBl5tfyHI033U5jONS9DuN+8=
go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM=
go.step.sm/linkedca v0.19.0-rc.1 h1:8XcQvanelK1g0ijl5/itmmAIsqD2QSMHGqcWzJwwJCU=
go.step.sm/linkedca v0.19.0-rc.1/go.mod h1:G35baT7Qnh6VsRCjzSfi5xsYw0ERrU+I1aIuZswMBeA=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=

Loading…
Cancel
Save