forked from TrueCloudLab/certificates
commit
baeb053eca
8 changed files with 568 additions and 22 deletions
|
@ -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 "" }
|
||||
|
|
|
@ -350,7 +350,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) {
|
||||
|
@ -378,7 +378,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) {
|
||||
|
@ -444,13 +444,17 @@ type appleAttestationData struct {
|
|||
Certificate *x509.Certificate
|
||||
}
|
||||
|
||||
func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*appleAttestationData, error) {
|
||||
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 = x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
}
|
||||
|
||||
x5c, ok := att.AttStatement["x5c"].([]interface{})
|
||||
if !ok {
|
||||
|
@ -541,13 +545,17 @@ type stepAttestationData struct {
|
|||
SerialNumber string
|
||||
}
|
||||
|
||||
func doStepAttestationFormat(ctx context.Context, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
|
||||
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 = 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 {
|
||||
|
|
|
@ -3,6 +3,7 @@ package provisioner
|
|||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
@ -95,9 +96,13 @@ type ACME struct {
|
|||
// provisioner. If this value is not set the default apple, step and tpm
|
||||
// will be used.
|
||||
AttestationFormats []ACMEAttestationFormat `json:"attestationFormats,omitempty"`
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -166,6 +171,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
|
||||
}
|
||||
|
@ -282,3 +310,12 @@ 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.
|
||||
//
|
||||
// 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, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
14
authority/provisioner/testdata/certs/apple-att-ca.crt
vendored
Normal file
14
authority/provisioner/testdata/certs/apple-att-ca.crt
vendored
Normal file
|
@ -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-----
|
19
authority/provisioner/testdata/certs/yubico-piv-ca.crt
vendored
Normal file
19
authority/provisioner/testdata/certs/yubico-piv-ca.crt
vendored
Normal file
|
@ -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-----
|
Loading…
Reference in a new issue